reservation implementation

This commit is contained in:
root
2026-06-11 15:35:25 -04:00
parent 6eeba99c45
commit 37a6ed8a76
30 changed files with 2598 additions and 477 deletions
@@ -0,0 +1,120 @@
# Car Booking Process Revision and Improvement Plan
## Problem
The current renter booking process is too heavy too early. It asks for identity documents, full address, and detailed license data before the renter has even submitted a reservation request. That creates unnecessary friction, lowers conversion, and makes the flow feel like paperwork instead of booking.
## Revision Applied
The booking form has been simplified into a low-friction request flow:
1. Trip details first: pick-up location, drop-off choice, pick-up date, return date, and estimated total.
2. Contact details second: first name, last name, email, phone, and optional notes.
3. Driver details moved into an optional collapsible section.
4. Submission copy changed from a formal “reservation request” flow to a faster “request this car in 1 minute” flow.
5. Required validation reduced to only what is necessary to start the booking conversation: dates, name, email, phone, and required locations.
6. Optional driver fields are still supported and sent to the same backend if the renter chooses to provide them.
7. The existing backend contract is preserved because those identity and license fields are already optional in the public reservation schema.
## Why This Is Better
The old flow forced renters to provide high-trust personal data before they had enough confidence that the booking was worth completing. That is backwards. The revised flow asks only for the information needed to check availability and contact the renter. Driver documents can be collected after the company confirms availability, pricing, and intent.
This reduces cognitive load, reduces privacy anxiety, and makes mobile completion more realistic. Yes, apparently people abandon forms when confronted with a small government census inside a car card. Shocking.
## Implementation Plan
### Phase 1: Frontend Simplification
Replace the current booking form with the revised version in:
`apps/marketplace/src/components/BookingForm.tsx`
Acceptance criteria:
- The visible default form fits into three clear sections: trip details, contact details, optional driver details.
- Driver identity and license fields are hidden by default.
- Required fields are only trip dates, renter name, email, phone, and required locations.
- Estimated total still updates when valid dates are entered.
- Existing helper behavior for total days, return location, and optional text normalization remains intact.
### Phase 2: Availability Confidence
Add a lightweight availability check before or during submission.
Recommended behavior:
- When dates are selected, call an availability endpoint for the selected vehicle and date range.
- If unavailable, show the next available date before the renter fills contact details.
- If available, show a small “Likely available” message, without overpromising final confirmation.
Backend candidate:
- Add or expose `GET /marketplace/:slug/vehicles/:id/availability?startDate=&endDate=`.
- Reuse the existing `getVehicleAvailabilitySummary` service instead of inventing yet another inconsistent truth source, because apparently systems enjoy lying to themselves.
### Phase 3: Booking Status Transparency
After submission, show exactly what happens next.
Suggested post-submit states:
- Request sent.
- Company confirms availability.
- Renter provides documents if required.
- Payment/deposit is completed.
- Booking confirmed.
Acceptance criteria:
- The renter understands that the first submission is a request, not a guaranteed reservation.
- Staff can see incomplete document status inside the dashboard.
- Reminder notifications are triggered if documents or payment are missing.
### Phase 4: Save Renter Profile
For logged-in renters, prefill known data.
Recommended behavior:
- Prefill name, email, and phone from the renter profile.
- Let renters save driver details to their account after submitting once.
- Never require repeated entry of license and identity fields for returning renters unless expired or missing.
### Phase 5: Measure the Flow
Track funnel metrics so the team stops guessing, a radical concept.
Recommended events:
- `booking_form_viewed`
- `trip_dates_selected`
- `contact_details_started`
- `driver_details_expanded`
- `booking_request_submitted`
- `booking_request_failed`
- `booking_request_success`
Primary KPIs:
- Form completion rate.
- Time to submit.
- Drop-off rate before contact details.
- Drop-off rate after driver details expansion.
- Request-to-confirmation conversion.
## Future UX Improvements
- Add date and time selection, not just dates, because rental handoff depends on time.
- Add promo code validation inside the vehicle page, not only search.
- Add a sticky mobile bottom bar with price estimate and submit button.
- Add a progress indicator: Trip → Contact → Optional documents.
- Add WhatsApp fallback after successful submission for companies that rely on chat.
- Support document upload after confirmation rather than typing document numbers into a tiny form.
## Risk Notes
- Do not remove backend support for driver fields. Some companies may need them for compliance or faster approvals.
- Do not make the first request feel like a guaranteed booking unless inventory locking and payment are implemented.
- Do not collect sensitive identity data unless there is a clear business reason and proper privacy handling.
- Do not bury the total price. Price uncertainty kills trust faster than bad button colors, though humans keep trying both.
@@ -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 companys 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 companys 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 companys 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 companys 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 companys customers.
- Company dashboards cannot access another companys 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.
+244
View File
@@ -0,0 +1,244 @@
# User Input Validation and Formatting Plan
## Table of Contents
1. [Global Rules](#global-rules)
2. [Field-Specific Rules](#field-specific-rules)
3. [Implementation Logic](#implementation-logic)
4. [Formatting Functions](#formatting-functions)
5. [Error Handling](#error-handling)
6. [Processing Order](#processing-order)
7. [Field Configuration Reference](#field-configuration-reference)
---
## Global Rules
### Allowed Characters
All fields accept only:
- Letters (`A-Z`, `a-z`)
- Numbers (`0-9`)
- Special characters: `@`, `-`, `/`
- Spaces (where applicable)
**Global Regex Pattern:**
```regex
^[a-zA-Z0-9@\-\/\s]*$
Field-Specific Rules
Group 1: Title Case Fields (Uppercase First Letter, Lowercase Rest)
Fields: Name, Nationality, Street Address, City, Pick-up Location, Return Location, Country
Property Value
Format Uppercase first letter, lowercase rest
Max Characters 50 (Name, Nationality, City, Locations, Country) / 100 (Street Address)
Allowed Characters Letters, spaces, - (Street Address also allows numbers and /)
Transformation toTitleCase()
Examples:
"john doe" → "John Doe"
"MARIA" → "Maria"
"new york" → "New York"
"123 main st." → "123 Main St."
Group 2: Title Case All Words Fields
Fields: Full Address, Commercial Name, Legal Company Name
Property Value
Format Uppercase first letter of every word
Max Characters 200 (Full Address) / 100 (Company Names)
Allowed Characters Letters, numbers, spaces, -, /, ,
Transformation toTitleCaseAll()
Examples:
"123 main street, apt 4b" → "123 Main Street, Apt 4b"
"acme corporation" → "Acme Corporation"
Group 3: Lowercase Fields
Fields: Email
Property Value
Format All lowercase
Max Characters 100
Allowed Characters Letters, numbers, @, ., _, %, +, -
Transformation toLowerCase()
Validation Must match email format
Validation Regex:
regex
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
Examples:
"John.Doe@Example.COM" → "john.doe@example.com"
Group 4: Uppercase Fields (Letters Only)
Fields: License Plate, VIN, CIN, Passport Number, International Permit Number, Driver License Number, License Category, ICE Number, Commercial Registry (RC)
Property Value
Format ALL UPPERCASE letters, numbers unchanged
Max Characters 30
Allowed Characters Letters and numbers only
Transformation toUpperCase()
Examples:
"abc-123" → "ABC-123"
"ab123cd" → "AB123CD"
Group 5: Car Fields
Fields: Car Mark, Car Model, Car Color
Property Value
Format Uppercase first letter, lowercase rest
Max Characters 30
Allowed Characters Letters, numbers, spaces, -
Transformation toTitleCase()
Examples:
"TOYOTA" → "Toyota"
"midnight-blue" → "Midnight-Blue"
Group 6: Preserved Format Fields
Fields: ZIP Code
Property Value
Format As entered (no case transformation)
Max Characters 10
Allowed Characters Letters, numbers, -
Transformation preserveOriginal()
Examples:
"12345" → "12345"
"SW1A 1AA" → "SW1A 1AA"
Implementation Logic
Main Processing Function
javascript
function sanitizeAndFormatInput(value, fieldType) {
// Step 1: Remove disallowed characters
let sanitized = removeDisallowedChars(value, fieldType);
// Step 2: Trim leading/trailing whitespace
sanitized = sanitized.trim();
// Step 3: Validate format (if applicable)
if (fieldType === 'email' && !isValidEmail(sanitized)) {
throw new Error('Invalid email format');
}
// Step 4: Apply field-specific formatting
let formatted = applyFormatting(sanitized, fieldType);
// Step 5: Truncate to max length
formatted = truncateToMaxLength(formatted, fieldType);
return formatted;
}
Processing Order
Sanitize — Remove invalid characters
Trim — Remove leading/trailing whitespace
Validate — Check format (email, required fields)
Format — Apply case transformation
Truncate — Enforce max length
Return — Output formatted value
Formatting Functions
Function Description Used For
toTitleCase() First letter uppercase, rest lowercase per word Name, Nationality, Street Address, City, Locations, Country, Car fields
toTitleCaseAll() First letter of every word uppercase Full Address, Commercial Name, Legal Company Name
toLowerCase() All characters to lowercase Email
toUpperCase() All letters to uppercase, numbers unchanged License Plate, VIN, CIN, Passport, Permit, Driver License, License Category, ICE, RC
preserveOriginal() No transformation ZIP Code
Function Implementations
javascript
function toTitleCase(str) {
return str.replace(/\b\w/g, char => char.toUpperCase())
.replace(/\b\w+\b/g, word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
}
function toTitleCaseAll(str) {
return str.replace(/\b\w+\b/g, word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
}
function toLowerCase(str) {
return str.toLowerCase();
}
function toUpperCase(str) {
return str.replace(/[a-zA-Z]/g, char => char.toUpperCase());
}
Error Handling
Condition Error Message
Invalid characters "Only letters, numbers, @, -, and / are allowed"
Invalid email format "Please enter a valid email address"
Required field empty "This field is required"
Exceeds max length "Maximum [X] characters allowed"
Validation Flow
text
User Input
Character Validation ────► Invalid ────► Error Message
Format Validation ────► Invalid ────► Error Message
Apply Transformation
Check Max Length ────► Exceeds ────► Truncate / Warning
Return Formatted Value
Field Configuration Reference
# Field Max Length Format Transformation
1 Name 50 Title Case toTitleCase()
2 Nationality 50 Title Case toTitleCase()
3 Street Address 100 Title Case toTitleCase()
4 City 50 Title Case toTitleCase()
5 Pick-up Location 50 Title Case toTitleCase()
6 Return Location 50 Title Case toTitleCase()
7 Country 50 Title Case toTitleCase()
8 Full Address 200 Title Case All toTitleCaseAll()
9 Email 100 Lowercase toLowerCase()
10 Car Mark 30 Title Case toTitleCase()
11 Car Model 30 Title Case toTitleCase()
12 Car Color 30 Title Case toTitleCase()
13 License Plate 30 Uppercase toUpperCase()
14 VIN 30 Uppercase toUpperCase()
15 CIN 30 Uppercase toUpperCase()
16 Passport Number 30 Uppercase toUpperCase()
17 International Permit Number 30 Uppercase toUpperCase()
18 Driver License Number 30 Uppercase toUpperCase()
19 License Category 30 Uppercase toUpperCase()
20 ICE Number 30 Uppercase toUpperCase()
21 Commercial Registry (RC) 30 Uppercase toUpperCase()
22 Commercial Name 100 Title Case All toTitleCaseAll()
23 Legal Company Name 100 Title Case All toTitleCaseAll()
24 ZIP Code 10 Preserved preserveOriginal()
Quick Reference Card
Email Validation
regex
^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
Global Character Allowlist
regex
^[a-zA-Z0-9@\-\/\s]*$
Rules Summary
Group Fields Transformation
Title Case Name, Nationality, Address, City, Locations, Country "John Doe"
Title Case All Full Address, Company Names "Acme Corporation Ltd."
Lowercase Email "user@domain.com"
Uppercase IDs, Licenses, Documents "ABC123"
Preserved ZIP Code As entered