reservation implementation
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
# Plan: Fix and Simplify Car Booking Without Adding Customer Accounts
|
||||
|
||||
## Objective
|
||||
|
||||
Improve the car booking process while keeping the current guest booking model.
|
||||
|
||||
This plan does **not** add:
|
||||
|
||||
- Customer accounts
|
||||
- Customer login
|
||||
- Renter dashboard
|
||||
- Global customer profile
|
||||
- Cross-company customer sharing
|
||||
- Saved customer identity
|
||||
|
||||
The goal is to make booking smoother, safer, and simpler while keeping each company’s customer data private.
|
||||
|
||||
---
|
||||
|
||||
## Current Direction
|
||||
|
||||
The system should continue using this structure:
|
||||
|
||||
```txt
|
||||
Company
|
||||
└── Customer
|
||||
└── Reservation
|
||||
```
|
||||
|
||||
A customer who books with Company A exists as a customer under Company A.
|
||||
|
||||
If the same person books with Company B, they become a separate customer under Company B.
|
||||
|
||||
This is the correct approach for now.
|
||||
|
||||
---
|
||||
|
||||
## Core Privacy Rule
|
||||
|
||||
Customer data must always belong to one company.
|
||||
|
||||
The system must never allow Company A to view, edit, search, or infer customer data from Company B.
|
||||
|
||||
Correct model:
|
||||
|
||||
```txt
|
||||
Company A
|
||||
└── john@example.com
|
||||
|
||||
Company B
|
||||
└── john@example.com
|
||||
```
|
||||
|
||||
These are two separate customer records.
|
||||
|
||||
Do **not** create a shared global customer record.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Keep Guest Booking Only
|
||||
|
||||
### Decision
|
||||
|
||||
Do not build customer accounts now.
|
||||
|
||||
Booking should work without login.
|
||||
|
||||
The customer only submits the information needed to request a booking.
|
||||
|
||||
### Required Behavior
|
||||
|
||||
When a customer books a car:
|
||||
|
||||
1. Customer selects a car.
|
||||
2. Customer enters basic booking and contact information.
|
||||
3. Backend finds the car.
|
||||
4. Backend derives the company from the car.
|
||||
5. Backend finds or creates a customer under that company.
|
||||
6. Backend creates a reservation under that company.
|
||||
|
||||
The customer should not need to create an account to make a booking.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Simplify the Booking Form
|
||||
|
||||
### Problem
|
||||
|
||||
The current booking process asks for too much information too early.
|
||||
|
||||
That creates friction and makes the booking feel heavier than it needs to be.
|
||||
|
||||
### Fix
|
||||
|
||||
The first booking form should only ask for:
|
||||
|
||||
- Name
|
||||
- Email
|
||||
- Phone
|
||||
- Pickup date
|
||||
- Return date
|
||||
- Pickup location
|
||||
- Return location
|
||||
- Optional message
|
||||
|
||||
### Move These Fields Out of the First Step
|
||||
|
||||
Do not require these during the initial booking request:
|
||||
|
||||
- Driver license
|
||||
- License expiration date
|
||||
- Government ID
|
||||
- Passport
|
||||
- Full address
|
||||
- Document upload
|
||||
- Payment details
|
||||
|
||||
These can be requested later during verification or approval.
|
||||
|
||||
### New Booking Flow
|
||||
|
||||
```txt
|
||||
Step 1: Request Booking
|
||||
- Customer contact info
|
||||
- Trip dates
|
||||
- Pickup and return locations
|
||||
- Notes
|
||||
|
||||
Step 2: Company Reviews Request
|
||||
- Company checks availability
|
||||
- Company accepts, rejects, or asks for more details
|
||||
|
||||
Step 3: Verification If Needed
|
||||
- License
|
||||
- Address
|
||||
- Documents
|
||||
- Payment
|
||||
```
|
||||
|
||||
This keeps the first action lightweight.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Derive Company Ownership From the Car
|
||||
|
||||
### Problem
|
||||
|
||||
The frontend should not decide which company owns a reservation.
|
||||
|
||||
If the frontend sends `companyId`, it can be wrong, manipulated, or stale.
|
||||
|
||||
### Fix
|
||||
|
||||
The backend must derive `companyId` from the selected car.
|
||||
|
||||
Correct backend logic:
|
||||
|
||||
```ts
|
||||
const car = await prisma.car.findUnique({
|
||||
where: { id: input.carId },
|
||||
select: {
|
||||
id: true,
|
||||
companyId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!car) {
|
||||
throw new Error("Car not found");
|
||||
}
|
||||
|
||||
const companyId = car.companyId;
|
||||
```
|
||||
|
||||
Then use that `companyId` for both the customer and reservation.
|
||||
|
||||
The frontend may send `carId`, but the backend decides the company.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Find or Create Customer by Company and Email
|
||||
|
||||
### Required Rule
|
||||
|
||||
Customers should be unique inside each company, not globally.
|
||||
|
||||
Use:
|
||||
|
||||
```prisma
|
||||
@@unique([companyId, email])
|
||||
```
|
||||
|
||||
This allows:
|
||||
|
||||
```txt
|
||||
john@example.com under Company A
|
||||
john@example.com under Company B
|
||||
```
|
||||
|
||||
without mixing their records.
|
||||
|
||||
### Booking Logic
|
||||
|
||||
```ts
|
||||
const normalizedEmail = input.email.toLowerCase().trim();
|
||||
|
||||
const customer = await prisma.customer.upsert({
|
||||
where: {
|
||||
companyId_email: {
|
||||
companyId,
|
||||
email: normalizedEmail,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
name: input.name,
|
||||
phone: input.phone,
|
||||
},
|
||||
create: {
|
||||
companyId,
|
||||
email: normalizedEmail,
|
||||
name: input.name,
|
||||
phone: input.phone,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
Do not use email alone to find a customer.
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
where: {
|
||||
email: input.email
|
||||
}
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
where: {
|
||||
companyId_email: {
|
||||
companyId,
|
||||
email: normalizedEmail
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Create Reservation With Company Scope
|
||||
|
||||
Every reservation must be linked to:
|
||||
|
||||
- `companyId`
|
||||
- `customerId`
|
||||
- `carId`
|
||||
|
||||
Recommended reservation creation:
|
||||
|
||||
```ts
|
||||
const reservation = await prisma.reservation.create({
|
||||
data: {
|
||||
companyId,
|
||||
customerId: customer.id,
|
||||
carId: car.id,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
pickupLocation: input.pickupLocation,
|
||||
returnLocation: input.returnLocation,
|
||||
notes: input.notes,
|
||||
status: "PENDING",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Initial reservation status should be:
|
||||
|
||||
```txt
|
||||
PENDING
|
||||
```
|
||||
|
||||
The company can later change it to:
|
||||
|
||||
```txt
|
||||
APPROVED
|
||||
REJECTED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Enforce Company Privacy in Dashboard Queries
|
||||
|
||||
### Problem
|
||||
|
||||
If company dashboard routes fetch customer or reservation records only by ID, one company may accidentally access another company’s data.
|
||||
|
||||
That is a serious multi-tenant privacy bug.
|
||||
|
||||
### Fix
|
||||
|
||||
Every company-facing query must include `companyId`.
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
});
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
await prisma.customer.findFirst({
|
||||
where: {
|
||||
id: customerId,
|
||||
companyId: currentCompany.id,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
await prisma.reservation.findUnique({
|
||||
where: { id: reservationId },
|
||||
});
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```ts
|
||||
await prisma.reservation.findFirst({
|
||||
where: {
|
||||
id: reservationId,
|
||||
companyId: currentCompany.id,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Apply This Rule To
|
||||
|
||||
- Customers
|
||||
- Reservations
|
||||
- Cars
|
||||
- Documents
|
||||
- Notes
|
||||
- Payments
|
||||
- Messages
|
||||
- Invoices
|
||||
- Reviews
|
||||
|
||||
Anything owned by a company must be queried with company scope.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Prevent Cross-Company Search
|
||||
|
||||
Company staff should never be able to search all customers globally.
|
||||
|
||||
Company customer search must always filter by the company.
|
||||
|
||||
Correct:
|
||||
|
||||
```ts
|
||||
await prisma.customer.findMany({
|
||||
where: {
|
||||
companyId: currentCompany.id,
|
||||
OR: [
|
||||
{ name: { contains: search, mode: "insensitive" } },
|
||||
{ email: { contains: search, mode: "insensitive" } },
|
||||
{ phone: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Incorrect:
|
||||
|
||||
```ts
|
||||
await prisma.customer.findMany({
|
||||
where: {
|
||||
email: { contains: search },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The second version searches across companies and should not exist.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Improve Booking Status Feedback
|
||||
|
||||
After submission, show a clear confirmation page.
|
||||
|
||||
The customer should see:
|
||||
|
||||
- Booking reference
|
||||
- Car name
|
||||
- Pickup date
|
||||
- Return date
|
||||
- Status: Pending
|
||||
- Company name
|
||||
- Message: The company will review your request.
|
||||
|
||||
Do not leave the customer wondering whether the booking worked.
|
||||
|
||||
### Optional Confirmation Email
|
||||
|
||||
Send a confirmation email with:
|
||||
|
||||
- Booking reference
|
||||
- Car details
|
||||
- Dates
|
||||
- Company name
|
||||
- Status
|
||||
- Support or contact information
|
||||
|
||||
No customer account required.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Add Booking Reference
|
||||
|
||||
Add a public-safe booking reference to reservations.
|
||||
|
||||
Example:
|
||||
|
||||
```txt
|
||||
BK-2026-8F3K2
|
||||
```
|
||||
|
||||
This is useful for emails, support, and later booking lookup.
|
||||
|
||||
Recommended field:
|
||||
|
||||
```prisma
|
||||
bookingReference String @unique
|
||||
```
|
||||
|
||||
The reference should not expose database IDs.
|
||||
|
||||
Bad reference:
|
||||
|
||||
```txt
|
||||
cm8x9customeridreservationid
|
||||
```
|
||||
|
||||
Good reference:
|
||||
|
||||
```txt
|
||||
BK-7H92KQ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Optional Booking Lookup Without Account
|
||||
|
||||
If needed later, allow customers to check booking status without creating an account.
|
||||
|
||||
Use:
|
||||
|
||||
```txt
|
||||
booking reference + email
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```txt
|
||||
Booking Reference: BK-7H92KQ
|
||||
Email: john@example.com
|
||||
```
|
||||
|
||||
The backend should only return limited information:
|
||||
|
||||
- Status
|
||||
- Car name
|
||||
- Company name
|
||||
- Pickup date
|
||||
- Return date
|
||||
- Pickup location
|
||||
- Return location
|
||||
|
||||
Do not expose:
|
||||
|
||||
- Internal notes
|
||||
- Admin comments
|
||||
- Risk flags
|
||||
- Documents
|
||||
- Payment metadata
|
||||
- Other bookings
|
||||
- Other company records
|
||||
|
||||
This gives convenience without building full accounts.
|
||||
|
||||
---
|
||||
|
||||
## Phase 11: Add Validation
|
||||
|
||||
Validate booking input on both frontend and backend.
|
||||
|
||||
### Required Validations
|
||||
|
||||
- Email must be valid.
|
||||
- Phone must be valid enough for contact.
|
||||
- Start date must be before end date.
|
||||
- Start date cannot be in the past.
|
||||
- Car must exist.
|
||||
- Car must be active/listed.
|
||||
- Car must belong to an active company.
|
||||
|
||||
### Date Validation
|
||||
|
||||
Reject:
|
||||
|
||||
- Return date before pickup date.
|
||||
- Same-day invalid ranges if not allowed.
|
||||
- Past pickup dates.
|
||||
|
||||
### Availability Validation
|
||||
|
||||
Before creating a reservation, check whether the car already has an overlapping approved or pending reservation.
|
||||
|
||||
Example logic:
|
||||
|
||||
```ts
|
||||
const overlappingReservation = await prisma.reservation.findFirst({
|
||||
where: {
|
||||
carId: car.id,
|
||||
status: {
|
||||
in: ["PENDING", "APPROVED"],
|
||||
},
|
||||
startDate: {
|
||||
lt: input.endDate,
|
||||
},
|
||||
endDate: {
|
||||
gt: input.startDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (overlappingReservation) {
|
||||
throw new Error("Car is not available for the selected dates");
|
||||
}
|
||||
```
|
||||
|
||||
This avoids double booking.
|
||||
|
||||
---
|
||||
|
||||
## Phase 12: Add Tests
|
||||
|
||||
Add tests for the booking flow and company privacy.
|
||||
|
||||
### Test 1: Booking creates company-scoped customer
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
Company A
|
||||
Car A belongs to Company A
|
||||
Customer books Car A
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
Customer is created under Company A
|
||||
Reservation is created under Company A
|
||||
```
|
||||
|
||||
### Test 2: Same email can book with two companies
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
john@example.com books with Company A
|
||||
john@example.com books with Company B
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
Two customer records exist
|
||||
One under Company A
|
||||
One under Company B
|
||||
```
|
||||
|
||||
### Test 3: Company cannot view another company’s customer
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
Customer belongs to Company B
|
||||
Company A user tries to fetch that customer
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
404 or 403
|
||||
```
|
||||
|
||||
### Test 4: Company cannot view another company’s reservation
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
Reservation belongs to Company B
|
||||
Company A user tries to fetch it
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
404 or 403
|
||||
```
|
||||
|
||||
### Test 5: Backend ignores frontend companyId
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
Frontend sends carId for Company A
|
||||
Frontend also sends companyId for Company B
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
Reservation is created under Company A
|
||||
```
|
||||
|
||||
The backend must trust the car ownership, not the frontend payload.
|
||||
|
||||
### Test 6: Double booking is blocked
|
||||
|
||||
Given:
|
||||
|
||||
```txt
|
||||
Car has an approved reservation from June 10 to June 15
|
||||
Customer tries to book June 12 to June 14
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```txt
|
||||
Booking is rejected
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 13: Implementation Order
|
||||
|
||||
1. Audit current booking form.
|
||||
2. Classify fields as required now, optional now, move to verification, or remove.
|
||||
3. Simplify the frontend booking form.
|
||||
4. Update booking API to accept `carId` and derive `companyId` from the car.
|
||||
5. Normalize email before lookup or storage.
|
||||
6. Find or create customer using `companyId + email`.
|
||||
7. Create reservation with `companyId + customerId + carId`.
|
||||
8. Set initial reservation status to `PENDING`.
|
||||
9. Audit company dashboard routes.
|
||||
10. Add `companyId` filtering to every company-owned query.
|
||||
11. Prevent cross-company customer and reservation search.
|
||||
12. Add reservation availability check.
|
||||
13. Add booking confirmation page.
|
||||
14. Add confirmation email if email infrastructure exists.
|
||||
15. Add booking reference if not already present.
|
||||
16. Add tests for booking creation, company isolation, and double-booking prevention.
|
||||
17. Deploy carefully.
|
||||
18. Run migration only if schema changes are needed.
|
||||
|
||||
---
|
||||
|
||||
## What Not To Change Now
|
||||
|
||||
Do not add:
|
||||
|
||||
- Renter model usage
|
||||
- Customer login
|
||||
- Customer password
|
||||
- Customer dashboard
|
||||
- Shared profile
|
||||
- Cross-company booking history
|
||||
- Saved documents
|
||||
- Saved payment methods
|
||||
|
||||
Do not change customer uniqueness to global email.
|
||||
|
||||
Do not let companies search customers outside their own company.
|
||||
|
||||
Do not trust `companyId` from the frontend.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
This work is complete when:
|
||||
|
||||
- A customer can book without creating an account.
|
||||
- The booking form is shorter and easier to complete.
|
||||
- Customers are created per company using `companyId + email`.
|
||||
- The same email can exist under multiple companies.
|
||||
- Reservations are always tied to the correct company.
|
||||
- The backend derives `companyId` from `carId`.
|
||||
- Company dashboards cannot access another company’s customers.
|
||||
- Company dashboards cannot access another company’s reservations.
|
||||
- Overlapping bookings are blocked.
|
||||
- Booking confirmation is clear.
|
||||
- Sensitive information is not collected too early.
|
||||
|
||||
---
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
Keep the system guest-based for now.
|
||||
|
||||
Fix the booking process by simplifying the form, enforcing company-scoped data access, deriving company ownership from the car, and preventing double bookings.
|
||||
|
||||
This gives the product a cleaner booking process without adding customer accounts before they are actually needed.
|
||||
Reference in New Issue
Block a user