diff --git a/apps/admin/src/app/dashboard/companies/page.tsx b/apps/admin/src/app/dashboard/companies/page.tsx
index b62250b..75d85b1 100644
--- a/apps/admin/src/app/dashboard/companies/page.tsx
+++ b/apps/admin/src/app/dashboard/companies/page.tsx
@@ -11,6 +11,7 @@ interface Company {
slug: string
status: string
email: string
+ contractSettings: { legalName: string | null } | null
subscription: { plan: string; status: string } | null
_count: { employees: number; vehicles: number }
}
@@ -32,6 +33,7 @@ export default function AdminCompaniesPage() {
loading: 'Loading…',
empty: 'No companies found',
company: 'Company',
+ legalName: 'Legal name',
slug: 'Slug',
status: 'Status',
plan: 'Plan',
@@ -47,6 +49,7 @@ export default function AdminCompaniesPage() {
loading: 'Chargement…',
empty: 'Aucune entreprise trouvée',
company: 'Entreprise',
+ legalName: 'Raison sociale',
slug: 'Slug',
status: 'Statut',
plan: 'Plan',
@@ -62,6 +65,7 @@ export default function AdminCompaniesPage() {
loading: 'جارٍ التحميل…',
empty: 'لم يتم العثور على شركات',
company: 'الشركة',
+ legalName: 'الاسم القانوني',
slug: 'Slug',
status: 'الحالة',
plan: 'الخطة',
@@ -162,6 +166,9 @@ export default function AdminCompaniesPage() {
|
{c.name}
+ {c.contractSettings?.legalName && c.contractSettings.legalName !== c.name ? (
+ {copy.legalName}: {c.contractSettings.legalName}
+ ) : null}
{c.email}
|
{c.slug} |
diff --git a/apps/api/src/modules/admin/admin.repo.ts b/apps/api/src/modules/admin/admin.repo.ts
index ebcbd14..2b5dbea 100644
--- a/apps/api/src/modules/admin/admin.repo.ts
+++ b/apps/api/src/modules/admin/admin.repo.ts
@@ -2,6 +2,7 @@ import { prisma } from '../../lib/prisma'
const companyListInclude = {
brand: { select: { displayName: true, logoUrl: true, subdomain: true } },
+ contractSettings: { select: { legalName: true } },
subscription: { select: { plan: true, status: true } },
_count: { select: { employees: true, vehicles: true } },
} as const
@@ -125,9 +126,27 @@ export function getCompanyUpdateSnapshot(id: string) {
})
}
-export async function applyCompanyUpdate(id: string, body: any, current: { name: string; slug: string; brand?: { paymentMethodsEnabled?: any[] | null } | null }) {
+export async function applyCompanyUpdate(
+ id: string,
+ body: any,
+ current: {
+ name: string
+ slug: string
+ address?: unknown
+ brand?: { paymentMethodsEnabled?: any[] | null } | null
+ },
+) {
return prisma.$transaction(async (tx) => {
- if (body.company) await tx.company.update({ where: { id }, data: body.company })
+ if (body.company) {
+ const companyData = { ...body.company }
+ if (companyData.address && typeof companyData.address === 'object' && !Array.isArray(companyData.address)) {
+ const baseAddress = current.address && typeof current.address === 'object' && !Array.isArray(current.address)
+ ? current.address as Record
+ : {}
+ companyData.address = { ...baseAddress, ...companyData.address }
+ }
+ await tx.company.update({ where: { id }, data: companyData })
+ }
if (body.subscription) {
const sub = body.subscription
diff --git a/apps/api/src/modules/admin/admin.schemas.ts b/apps/api/src/modules/admin/admin.schemas.ts
index 0a4c02d..9777895 100644
--- a/apps/api/src/modules/admin/admin.schemas.ts
+++ b/apps/api/src/modules/admin/admin.schemas.ts
@@ -112,6 +112,30 @@ export const adminCompanyUpdateSchema = z.object({
email: z.string().email().optional(), phone: nullableString,
status: z.enum(['PENDING', 'TRIALING', 'ACTIVE', 'PAST_DUE', 'SUSPENDED', 'CANCELLED']).optional(),
subscriptionPaymentRef: nullableString,
+ address: z.object({
+ streetAddress: nullableString,
+ city: nullableString,
+ country: nullableString,
+ zipCode: nullableString,
+ legalName: nullableString,
+ legalForm: nullableString,
+ companyEmail: nullableEmail,
+ managerName: nullableString,
+ iceNumber: nullableString,
+ operatingLicenseNumber: nullableString,
+ operatingLicenseIssuedAt: nullableString,
+ operatingLicenseIssuedBy: nullableString,
+ fax: nullableString,
+ yearsActive: nullableString,
+ representativeName: nullableString,
+ representativeTitle: nullableString,
+ responsibleName: nullableString,
+ responsibleRole: nullableString,
+ responsibleIdentityNumber: nullableString,
+ responsibleQualification: nullableString,
+ responsiblePhone: nullableString,
+ responsibleEmail: nullableEmail,
+ }).optional(),
}).optional(),
subscription: z.object({
plan: z.enum(['STARTER', 'GROWTH', 'PRO']).optional(),
diff --git a/apps/api/src/modules/marketplace/marketplace.repo.ts b/apps/api/src/modules/marketplace/marketplace.repo.ts
index be7ee6c..bd56e64 100644
--- a/apps/api/src/modules/marketplace/marketplace.repo.ts
+++ b/apps/api/src/modules/marketplace/marketplace.repo.ts
@@ -49,13 +49,59 @@ export async function findVehicleForMarketplace(vehicleId: string, companySlug:
}
export async function upsertMarketplaceCustomer(companyId: string, data: {
- email: string; firstName: string; lastName: string; phone?: string
+ email: string
+ firstName: string
+ lastName: string
+ phone?: string
+ dateOfBirth?: string
+ nationality?: string
+ identityDocumentNumber?: string
+ fullAddress?: string
+ driverLicense?: string
+ licenseExpiry?: string
+ licenseIssuedAt?: string
+ licenseCountry?: string
+ licenseCategory?: string
+ internationalLicenseNumber?: string
}) {
- return prisma.customer.upsert({
+ const existing = await prisma.customer.findUnique({
where: { companyId_email: { companyId, email: data.email } },
- create: { companyId, firstName: data.firstName, lastName: data.lastName, email: data.email, phone: data.phone },
- update: { firstName: data.firstName, lastName: data.lastName, ...(data.phone ? { phone: data.phone } : {}) },
+ select: { id: true, address: true },
})
+
+ // Only patch the address JSON with fields that were actually provided
+ const prevAddress = (existing?.address && typeof existing.address === 'object' && !Array.isArray(existing.address))
+ ? existing.address as Record
+ : {}
+ const hasAddressPatch = data.fullAddress || data.identityDocumentNumber || data.internationalLicenseNumber !== undefined
+ const address = hasAddressPatch
+ ? {
+ ...prevAddress,
+ ...(data.fullAddress ? { fullAddress: data.fullAddress } : {}),
+ ...(data.identityDocumentNumber ? { identityDocumentNumber: data.identityDocumentNumber } : {}),
+ ...(data.internationalLicenseNumber !== undefined ? { internationalLicenseNumber: data.internationalLicenseNumber ?? null } : {}),
+ }
+ : undefined
+
+ const payload = {
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ ...(data.phone ? { phone: data.phone } : {}),
+ ...(data.driverLicense ? { driverLicense: data.driverLicense, licenseNumber: data.driverLicense } : {}),
+ ...(data.dateOfBirth ? { dateOfBirth: new Date(data.dateOfBirth) } : {}),
+ ...(data.nationality ? { nationality: data.nationality } : {}),
+ ...(address !== undefined ? { address } : {}),
+ ...(data.licenseExpiry ? { licenseExpiry: new Date(data.licenseExpiry) } : {}),
+ ...(data.licenseIssuedAt ? { licenseIssuedAt: new Date(data.licenseIssuedAt) } : {}),
+ ...(data.licenseCountry ? { licenseCountry: data.licenseCountry } : {}),
+ ...(data.licenseCategory ? { licenseCategory: data.licenseCategory } : {}),
+ }
+
+ if (!existing) {
+ return prisma.customer.create({ data: { companyId, ...payload } })
+ }
+ return prisma.customer.update({ where: { id: existing.id }, data: payload })
}
export async function createMarketplaceReservation(data: {
diff --git a/apps/api/src/modules/marketplace/marketplace.schemas.ts b/apps/api/src/modules/marketplace/marketplace.schemas.ts
index a958f46..4cb52ac 100644
--- a/apps/api/src/modules/marketplace/marketplace.schemas.ts
+++ b/apps/api/src/modules/marketplace/marketplace.schemas.ts
@@ -30,7 +30,19 @@ export const marketplaceReservationSchema = z.object({
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
email: z.string().email(),
- phone: z.string().max(30).optional(),
+ // Contact info — collected at reservation time
+ phone: z.string().min(1).max(30).optional(),
+ // Identity & license — optional at reservation, collected at pickup
+ dateOfBirth: z.string().datetime().optional(),
+ nationality: z.string().min(1).max(100).optional(),
+ identityDocumentNumber: z.string().min(1).max(100).optional(),
+ fullAddress: z.string().min(1).max(500).optional(),
+ driverLicense: z.string().min(1).max(50).optional(),
+ licenseExpiry: z.string().datetime().optional(),
+ licenseIssuedAt: z.string().datetime().optional(),
+ licenseCountry: z.string().min(1).max(100).optional(),
+ licenseCategory: z.string().min(1).max(20).optional(),
+ internationalLicenseNumber: z.string().trim().max(100).optional(),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
pickupLocation: z.string().trim().max(100).optional(),
diff --git a/apps/api/src/modules/marketplace/marketplace.service.ts b/apps/api/src/modules/marketplace/marketplace.service.ts
index 5cc6b7a..5f949d4 100644
--- a/apps/api/src/modules/marketplace/marketplace.service.ts
+++ b/apps/api/src/modules/marketplace/marketplace.service.ts
@@ -121,7 +121,11 @@ export async function searchVehicles(params: {
export async function createMarketplaceReservation(body: {
vehicleId: string; companySlug: string; firstName: string; lastName: string
- email: string; phone?: string; startDate: string; endDate: string
+ email: string; phone?: string; dateOfBirth?: string; nationality?: string
+ identityDocumentNumber?: string; fullAddress?: string
+ driverLicense?: string; licenseExpiry?: string; licenseIssuedAt?: string
+ licenseCountry?: string; licenseCategory?: string; internationalLicenseNumber?: string
+ startDate: string; endDate: string
pickupLocation?: string; returnLocation?: string; notes?: string; language?: string
}) {
const startDate = new Date(body.startDate)
@@ -160,7 +164,20 @@ export async function createMarketplaceReservation(body: {
}
const customer = await repo.upsertMarketplaceCustomer(vehicle.companyId, {
- email: body.email, firstName: body.firstName, lastName: body.lastName, phone: body.phone,
+ email: body.email,
+ firstName: body.firstName,
+ lastName: body.lastName,
+ phone: body.phone,
+ dateOfBirth: body.dateOfBirth,
+ nationality: body.nationality,
+ identityDocumentNumber: body.identityDocumentNumber,
+ fullAddress: body.fullAddress,
+ driverLicense: body.driverLicense,
+ licenseExpiry: body.licenseExpiry,
+ licenseIssuedAt: body.licenseIssuedAt,
+ licenseCountry: body.licenseCountry,
+ licenseCategory: body.licenseCategory,
+ internationalLicenseNumber: body.internationalLicenseNumber,
})
const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / 86_400_000))
diff --git a/apps/api/src/modules/marketplace/marketplace.test.ts b/apps/api/src/modules/marketplace/marketplace.test.ts
index f707eb5..7e2010e 100644
--- a/apps/api/src/modules/marketplace/marketplace.test.ts
+++ b/apps/api/src/modules/marketplace/marketplace.test.ts
@@ -121,6 +121,16 @@ describe('createMarketplaceReservation', () => {
const baseBody = {
vehicleId: 'v-1', companySlug: SLUG,
firstName: 'Ali', lastName: 'Ben', email: 'ali@test.com',
+ phone: '+212600000000',
+ dateOfBirth: '1990-01-01T00:00:00.000Z',
+ nationality: 'Moroccan',
+ identityDocumentNumber: 'CIN123456',
+ fullAddress: '123 Avenue Hassan II, Casablanca',
+ driverLicense: 'DL-123456',
+ licenseExpiry: '2030-01-01T00:00:00.000Z',
+ licenseIssuedAt: '2015-01-01T00:00:00.000Z',
+ licenseCountry: 'Morocco',
+ licenseCategory: 'B',
startDate: '2025-06-01T00:00:00.000Z', endDate: '2025-06-04T00:00:00.000Z',
}
@@ -132,6 +142,11 @@ describe('createMarketplaceReservation', () => {
const result = await createMarketplaceReservation({ ...baseBody, pickupLocation: 'Casablanca', returnLocation: 'Casablanca' })
expect(result.reservationId).toBe('r-1')
+ expect(repo.upsertMarketplaceCustomer).toHaveBeenCalledWith('co-1', expect.objectContaining({
+ identityDocumentNumber: 'CIN123456',
+ fullAddress: '123 Avenue Hassan II, Casablanca',
+ driverLicense: 'DL-123456',
+ }))
expect(repo.createMarketplaceReservation).toHaveBeenCalledWith(expect.objectContaining({
pickupLocation: 'Casablanca',
returnLocation: 'Casablanca',
diff --git a/apps/api/src/modules/reservations/reservation.lifecycle.service.ts b/apps/api/src/modules/reservations/reservation.lifecycle.service.ts
index aaf772d..0424a82 100644
--- a/apps/api/src/modules/reservations/reservation.lifecycle.service.ts
+++ b/apps/api/src/modules/reservations/reservation.lifecycle.service.ts
@@ -8,6 +8,17 @@ import { coerceNotificationLocale } from '../../services/notificationLocalizatio
import { buildReservationWorkflow, parseReservationExtras, serializeReservationForDashboard } from './reservation.presenter'
import * as repo from './reservation.repo'
+function buildPickupAddress(value: unknown) {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return ''
+
+ const address = value as Record
+ return [
+ typeof address.streetAddress === 'string' ? address.streetAddress.trim() : '',
+ typeof address.city === 'string' ? address.city.trim() : '',
+ typeof address.country === 'string' ? address.country.trim() : '',
+ ].filter(Boolean).join(', ')
+}
+
async function assertLicenseCompliance(reservationId: string, companyId: string) {
const reservation = await repo.findForLicenseCheck(reservationId, companyId)
@@ -46,6 +57,7 @@ export async function confirmReservation(id: string, companyId: string) {
repo.findVehicle(reservation.vehicleId, companyId),
])
const fallbackLocale = coerceNotificationLocale(company?.brand?.defaultLocale)
+ const pickupLocation = reservation.pickupLocation?.trim() || buildPickupAddress(company?.address)
await sendNotification({
type: 'BOOKING_CONFIRMED',
companyId,
@@ -57,8 +69,29 @@ export async function confirmReservation(id: string, companyId: string) {
templateVariables: {
firstName: customer.firstName,
companyName: company?.brand?.displayName ?? company?.name ?? 'RentalDriveGo',
- startDate: reservation.startDate,
- endDate: reservation.endDate,
+ startDate: {
+ type: 'date',
+ value: reservation.startDate,
+ options: {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ },
+ },
+ endDate: {
+ type: 'date',
+ value: reservation.endDate,
+ options: {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ },
+ },
+ pickupLocation,
vehicleName: `${vehicle.year} ${vehicle.make} ${vehicle.model}`,
},
}).catch(() => null)
diff --git a/apps/api/src/modules/reservations/reservation.test.ts b/apps/api/src/modules/reservations/reservation.test.ts
index 6bd1004..5df5ed4 100644
--- a/apps/api/src/modules/reservations/reservation.test.ts
+++ b/apps/api/src/modules/reservations/reservation.test.ts
@@ -72,6 +72,7 @@ import * as pricingService from './reservation.pricing.service'
import * as insuranceService from './reservation.insurance.service'
import * as additionalDriverService from './reservation.additional-driver.service'
import { validateLicense } from '../../services/licenseValidationService'
+import { sendNotification } from '../../services/notificationService'
import { buildReservationWorkflow } from './reservation.presenter'
import { createReservation } from './reservation.service'
import { confirmReservation, checkinReservation, checkoutReservation, closeReservation } from './reservation.lifecycle.service'
@@ -158,7 +159,7 @@ describe('createReservation', () => {
// ────────────────────────────────────────────────────────────────────────────
describe('confirmReservation', () => {
it('confirms a DRAFT reservation', async () => {
- const res = makeReservation({ status: 'DRAFT' })
+ const res = makeReservation({ status: 'DRAFT', pickupLocation: 'Casablanca Airport Terminal 1' })
vi.mocked(repo.findByIdSimple).mockResolvedValue(res as any)
vi.mocked(repo.findForLicenseCheck).mockResolvedValue({
...res,
@@ -174,6 +175,20 @@ describe('confirmReservation', () => {
const result = await confirmReservation(RES_ID, COMPANY)
expect(repo.updateById).toHaveBeenCalledWith(RES_ID, { status: 'CONFIRMED' })
+ expect(sendNotification).toHaveBeenCalledWith(expect.objectContaining({
+ templateKey: 'booking.confirmed',
+ templateVariables: expect.objectContaining({
+ pickupLocation: 'Casablanca Airport Terminal 1',
+ startDate: expect.objectContaining({
+ type: 'date',
+ value: res.startDate,
+ }),
+ endDate: expect.objectContaining({
+ type: 'date',
+ value: res.endDate,
+ }),
+ }),
+ }))
expect((result as any).status).toBe('CONFIRMED')
})
diff --git a/apps/api/src/modules/site/site.repo.ts b/apps/api/src/modules/site/site.repo.ts
index af990ec..0bb59d4 100644
--- a/apps/api/src/modules/site/site.repo.ts
+++ b/apps/api/src/modules/site/site.repo.ts
@@ -41,19 +41,61 @@ export async function findOfferByPromoCode(companyId: string, code: string) {
}
export async function upsertCustomer(companyId: string, data: {
- email: string; firstName: string; lastName: string; phone?: string | null
- driverLicense?: string | null; dateOfBirth?: string | null; licenseExpiry?: string | null
- licenseIssuedAt?: string | null; nationality?: string | null
+ email: string
+ firstName: string
+ lastName: string
+ phone: string
+ driverLicense: string
+ dateOfBirth: string
+ licenseExpiry: string
+ licenseIssuedAt: string
+ nationality: string
+ identityDocumentNumber: string
+ fullAddress: string
+ licenseCountry: string
+ licenseNumber?: string | null
+ licenseCategory: string
+ internationalLicenseNumber?: string | null
}) {
- const parsed = {
- dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : null,
- licenseExpiry: data.licenseExpiry ? new Date(data.licenseExpiry) : null,
- licenseIssuedAt: data.licenseIssuedAt ? new Date(data.licenseIssuedAt) : null,
- }
- return prisma.customer.upsert({
+ const existing = await prisma.customer.findUnique({
where: { companyId_email: { companyId, email: data.email } },
- create: { companyId, firstName: data.firstName, lastName: data.lastName, email: data.email, phone: data.phone ?? null, driverLicense: data.driverLicense ?? null, nationality: data.nationality ?? null, ...parsed },
- update: { firstName: data.firstName, lastName: data.lastName, phone: data.phone ?? null, driverLicense: data.driverLicense ?? null, nationality: data.nationality ?? null, ...parsed },
+ select: { id: true, address: true },
+ })
+
+ const address = {
+ ...(existing?.address && typeof existing.address === 'object' && !Array.isArray(existing.address)
+ ? existing.address as Record
+ : {}),
+ fullAddress: data.fullAddress,
+ identityDocumentNumber: data.identityDocumentNumber,
+ internationalLicenseNumber: data.internationalLicenseNumber ?? null,
+ }
+
+ const payload = {
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ phone: data.phone,
+ driverLicense: data.driverLicense,
+ dateOfBirth: new Date(data.dateOfBirth),
+ nationality: data.nationality,
+ address,
+ licenseExpiry: new Date(data.licenseExpiry),
+ licenseIssuedAt: new Date(data.licenseIssuedAt),
+ licenseCountry: data.licenseCountry,
+ licenseNumber: data.licenseNumber ?? data.driverLicense,
+ licenseCategory: data.licenseCategory,
+ }
+
+ if (!existing) {
+ return prisma.customer.create({
+ data: { companyId, ...payload },
+ })
+ }
+
+ return prisma.customer.update({
+ where: { id: existing.id },
+ data: payload,
})
}
diff --git a/apps/api/src/modules/site/site.schemas.ts b/apps/api/src/modules/site/site.schemas.ts
index 9a02ac8..bd4ae8d 100644
--- a/apps/api/src/modules/site/site.schemas.ts
+++ b/apps/api/src/modules/site/site.schemas.ts
@@ -18,12 +18,18 @@ export const bookSchema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
- phone: z.string().optional(),
- driverLicense: z.string().optional(),
- dateOfBirth: z.string().datetime().optional(),
- licenseExpiry: z.string().datetime().optional(),
- licenseIssuedAt: z.string().datetime().optional(),
- nationality: z.string().optional(),
+ phone: z.string().min(1).max(30),
+ driverLicense: z.string().min(1).max(50),
+ dateOfBirth: z.string().datetime(),
+ licenseExpiry: z.string().datetime(),
+ licenseIssuedAt: z.string().datetime(),
+ nationality: z.string().min(1).max(100),
+ identityDocumentNumber: z.string().min(1).max(100),
+ fullAddress: z.string().min(1).max(500),
+ licenseCountry: z.string().min(1).max(100),
+ licenseNumber: z.string().min(1).max(50).optional(),
+ licenseCategory: z.string().min(1).max(20),
+ internationalLicenseNumber: z.string().trim().max(100).optional(),
offerId: z.string().cuid().optional(),
promoCodeUsed: z.string().optional(),
notes: z.string().optional(),
diff --git a/apps/api/src/modules/site/site.service.ts b/apps/api/src/modules/site/site.service.ts
index 17bb6c4..aa39b49 100644
--- a/apps/api/src/modules/site/site.service.ts
+++ b/apps/api/src/modules/site/site.service.ts
@@ -93,8 +93,9 @@ export async function validatePromoCode(slug: string, code: string) {
export async function createBooking(slug: string, body: {
vehicleId: string; startDate: string; endDate: string
- firstName: string; lastName: string; email: string; phone?: string
- driverLicense?: string; dateOfBirth?: string; licenseExpiry?: string; licenseIssuedAt?: string; nationality?: string
+ firstName: string; lastName: string; email: string; phone: string
+ driverLicense: string; dateOfBirth: string; licenseExpiry: string; licenseIssuedAt: string; nationality: string
+ identityDocumentNumber: string; fullAddress: string; licenseCountry: string; licenseNumber?: string; licenseCategory: string; internationalLicenseNumber?: string
offerId?: string; promoCodeUsed?: string; notes?: string; source?: string
selectedInsurancePolicyIds?: string[]; additionalDrivers?: any[]
}) {
@@ -113,9 +114,21 @@ export async function createBooking(slug: string, body: {
}
const customer = await repo.upsertCustomer(company.id, {
- email: body.email, firstName: body.firstName, lastName: body.lastName, phone: body.phone,
- driverLicense: body.driverLicense, dateOfBirth: body.dateOfBirth,
- licenseExpiry: body.licenseExpiry, licenseIssuedAt: body.licenseIssuedAt, nationality: body.nationality,
+ email: body.email,
+ firstName: body.firstName,
+ lastName: body.lastName,
+ phone: body.phone,
+ driverLicense: body.driverLicense,
+ dateOfBirth: body.dateOfBirth,
+ licenseExpiry: body.licenseExpiry,
+ licenseIssuedAt: body.licenseIssuedAt,
+ nationality: body.nationality,
+ identityDocumentNumber: body.identityDocumentNumber,
+ fullAddress: body.fullAddress,
+ licenseCountry: body.licenseCountry,
+ licenseNumber: body.licenseNumber,
+ licenseCategory: body.licenseCategory,
+ internationalLicenseNumber: body.internationalLicenseNumber,
})
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86_400_000))
diff --git a/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx b/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx
index 7766a0b..4a4e4d3 100644
--- a/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx
+++ b/apps/dashboard/src/app/(dashboard)/reservations/new/page.tsx
@@ -13,9 +13,13 @@ type Customer = {
email: string
phone?: string | null
driverLicense?: string | null
+ dateOfBirth?: string | null
+ nationality?: string | null
+ address?: Record | null
licenseExpiry?: string | null
licenseIssuedAt?: string | null
licenseCountry?: string | null
+ licenseNumber?: string | null
licenseCategory?: string | null
licenseImageUrl?: string | null
}
@@ -62,6 +66,11 @@ export default function NewReservationPage() {
const [newCustomerEmail, setNewCustomerEmail] = useState('')
const [newCustomerPhone, setNewCustomerPhone] = useState('')
const [driverLicense, setDriverLicense] = useState('')
+ const [customerDateOfBirth, setCustomerDateOfBirth] = useState('')
+ const [customerNationality, setCustomerNationality] = useState('')
+ const [customerFullAddress, setCustomerFullAddress] = useState('')
+ const [customerIdentityDocumentNumber, setCustomerIdentityDocumentNumber] = useState('')
+ const [customerInternationalLicenseNumber, setCustomerInternationalLicenseNumber] = useState('')
const [licenseExpiry, setLicenseExpiry] = useState('')
const [licenseIssuedAt, setLicenseIssuedAt] = useState('')
const [licenseCountry, setLicenseCountry] = useState('')
@@ -94,12 +103,16 @@ export default function NewReservationPage() {
return: 'Return location',
deposit: 'Deposit amount (MAD)',
paymentMode: 'Payment mode',
+ renterIdentity: 'Renter identity',
driverLicenseInfo: 'Driver license information',
driverLicense: 'Driver license number',
licenseExpiry: 'License expiry',
licenseIssuedAt: 'License issued at',
licenseCountry: 'License country',
licenseCategory: 'License category',
+ fullAddress: 'Full address',
+ identityDocumentNumber: 'CIN / Passport number',
+ internationalLicenseNumber: 'International permit number',
licenseImage: 'Driver license image',
licenseImageHint: 'Upload a photo or scan of the primary driver license.',
licenseImageSelected: 'Selected file',
@@ -108,7 +121,7 @@ export default function NewReservationPage() {
addAdditionalDriver: 'Add additional driver',
additionalDriverInfo: 'Additional driver',
dateOfBirth: 'Date of birth',
- nationality: 'License country',
+ nationality: 'Nationality',
notes: 'Notes',
notesPlaceholder: 'Optional notes…',
create: 'Create booking',
@@ -148,12 +161,16 @@ export default function NewReservationPage() {
return: 'Lieu de retour',
deposit: 'Montant du dépôt (MAD)',
paymentMode: 'Mode de paiement',
+ renterIdentity: 'Identité du locataire',
driverLicenseInfo: 'Informations du permis',
driverLicense: 'Numéro du permis',
licenseExpiry: 'Expiration du permis',
licenseIssuedAt: 'Permis délivré le',
licenseCountry: 'Pays du permis',
licenseCategory: 'Catégorie du permis',
+ fullAddress: 'Adresse complète',
+ identityDocumentNumber: 'N° CIN / passeport',
+ internationalLicenseNumber: 'N° de permis international',
licenseImage: 'Image du permis',
licenseImageHint: 'Téléversez une photo ou un scan du permis du conducteur principal.',
licenseImageSelected: 'Fichier sélectionné',
@@ -162,7 +179,7 @@ export default function NewReservationPage() {
addAdditionalDriver: 'Ajouter un conducteur supplémentaire',
additionalDriverInfo: 'Conducteur supplémentaire',
dateOfBirth: 'Date de naissance',
- nationality: 'Pays du permis',
+ nationality: 'Nationalité',
notes: 'Notes',
notesPlaceholder: 'Notes optionnelles…',
create: 'Créer la réservation',
@@ -202,12 +219,16 @@ export default function NewReservationPage() {
return: 'موقع التسليم',
deposit: 'مبلغ العربون (MAD)',
paymentMode: 'طريقة الدفع',
+ renterIdentity: 'بيانات المستأجر',
driverLicenseInfo: 'معلومات رخصة القيادة',
driverLicense: 'رقم رخصة القيادة',
licenseExpiry: 'تاريخ انتهاء الرخصة',
licenseIssuedAt: 'تاريخ إصدار الرخصة',
licenseCountry: 'بلد الرخصة',
licenseCategory: 'فئة الرخصة',
+ fullAddress: 'العنوان الكامل',
+ identityDocumentNumber: 'رقم البطاقة الوطنية / جواز السفر',
+ internationalLicenseNumber: 'رقم الرخصة الدولية',
licenseImage: 'صورة رخصة القيادة',
licenseImageHint: 'ارفع صورة أو مسحًا لرخصة السائق الأساسي.',
licenseImageSelected: 'الملف المحدد',
@@ -216,7 +237,7 @@ export default function NewReservationPage() {
addAdditionalDriver: 'إضافة سائق إضافي',
additionalDriverInfo: 'السائق الإضافي',
dateOfBirth: 'تاريخ الميلاد',
- nationality: 'بلد الرخصة',
+ nationality: 'الجنسية',
notes: 'ملاحظات',
notesPlaceholder: 'ملاحظات اختيارية…',
create: 'إنشاء الحجز',
@@ -272,10 +293,22 @@ export default function NewReservationPage() {
const canSubmit = !!customerId && !!vehicleId && !!startDate && !!endDate
+ function readCustomerAddressValue(customer: Customer | undefined, key: string) {
+ const address = customer?.address
+ if (!address || typeof address !== 'object' || Array.isArray(address)) return ''
+ const value = address[key]
+ return typeof value === 'string' ? value : ''
+ }
+
useEffect(() => {
const selected = customers.find((customer) => customer.id === customerId)
if (!selected) return
- setDriverLicense(selected.driverLicense ?? '')
+ setDriverLicense(selected.driverLicense ?? selected.licenseNumber ?? '')
+ setCustomerDateOfBirth(selected.dateOfBirth ? selected.dateOfBirth.slice(0, 10) : '')
+ setCustomerNationality(selected.nationality ?? '')
+ setCustomerFullAddress(readCustomerAddressValue(selected, 'fullAddress'))
+ setCustomerIdentityDocumentNumber(readCustomerAddressValue(selected, 'identityDocumentNumber'))
+ setCustomerInternationalLicenseNumber(readCustomerAddressValue(selected, 'internationalLicenseNumber'))
setLicenseExpiry(selected.licenseExpiry ? selected.licenseExpiry.slice(0, 10) : '')
setLicenseIssuedAt(selected.licenseIssuedAt ? selected.licenseIssuedAt.slice(0, 10) : '')
setLicenseCountry(selected.licenseCountry ?? '')
@@ -318,6 +351,20 @@ export default function NewReservationPage() {
setError(copy.required)
return
}
+ if (
+ !driverLicense.trim() ||
+ !customerDateOfBirth ||
+ !customerNationality.trim() ||
+ !customerFullAddress.trim() ||
+ !customerIdentityDocumentNumber.trim() ||
+ !licenseExpiry ||
+ !licenseIssuedAt ||
+ !licenseCountry.trim() ||
+ !licenseCategory.trim()
+ ) {
+ setError(copy.required)
+ return
+ }
setSavingCustomer(true)
try {
const created = await apiFetch('/customers', {
@@ -329,11 +376,19 @@ export default function NewReservationPage() {
lastNameAr: newCustomerLastName.ar.trim() || undefined,
email: newCustomerEmail.trim(),
phone: newCustomerPhone.trim(),
- driverLicense: driverLicense.trim() || undefined,
- licenseExpiry: licenseExpiry ? new Date(licenseExpiry).toISOString() : undefined,
- licenseIssuedAt: licenseIssuedAt ? new Date(licenseIssuedAt).toISOString() : undefined,
- licenseCountry: licenseCountry.trim() || undefined,
- licenseCategory: licenseCategory.trim() || undefined,
+ driverLicense: driverLicense.trim(),
+ dateOfBirth: new Date(customerDateOfBirth).toISOString(),
+ nationality: customerNationality.trim(),
+ address: {
+ fullAddress: customerFullAddress.trim(),
+ identityDocumentNumber: customerIdentityDocumentNumber.trim(),
+ internationalLicenseNumber: customerInternationalLicenseNumber.trim() || undefined,
+ },
+ licenseExpiry: new Date(licenseExpiry).toISOString(),
+ licenseIssuedAt: new Date(licenseIssuedAt).toISOString(),
+ licenseCountry: licenseCountry.trim(),
+ licenseNumber: driverLicense.trim(),
+ licenseCategory: licenseCategory.trim(),
}),
})
setCustomers((prev) => [created, ...prev])
@@ -368,6 +423,20 @@ export default function NewReservationPage() {
setError(copy.invalidDates)
return
}
+ if (
+ !driverLicense.trim() ||
+ !customerDateOfBirth ||
+ !customerNationality.trim() ||
+ !customerFullAddress.trim() ||
+ !customerIdentityDocumentNumber.trim() ||
+ !licenseExpiry ||
+ !licenseIssuedAt ||
+ !licenseCountry.trim() ||
+ !licenseCategory.trim()
+ ) {
+ setError(copy.required)
+ return
+ }
if (includeAdditionalDriver && (!bilingualPrimary(additionalDriver.firstName).trim() || !bilingualPrimary(additionalDriver.lastName).trim() || !additionalDriver.driverLicense.trim())) {
setError(copy.required)
return
@@ -378,11 +447,19 @@ export default function NewReservationPage() {
await apiFetch(`/customers/${customerId}`, {
method: 'PATCH',
body: JSON.stringify({
- driverLicense: driverLicense.trim() || undefined,
- licenseExpiry: licenseExpiry ? new Date(licenseExpiry).toISOString() : undefined,
- licenseIssuedAt: licenseIssuedAt ? new Date(licenseIssuedAt).toISOString() : undefined,
- licenseCountry: licenseCountry.trim() || undefined,
- licenseCategory: licenseCategory.trim() || undefined,
+ driverLicense: driverLicense.trim(),
+ dateOfBirth: new Date(customerDateOfBirth).toISOString(),
+ nationality: customerNationality.trim(),
+ address: {
+ fullAddress: customerFullAddress.trim(),
+ identityDocumentNumber: customerIdentityDocumentNumber.trim(),
+ internationalLicenseNumber: customerInternationalLicenseNumber.trim() || undefined,
+ },
+ licenseExpiry: new Date(licenseExpiry).toISOString(),
+ licenseIssuedAt: new Date(licenseIssuedAt).toISOString(),
+ licenseCountry: licenseCountry.trim(),
+ licenseNumber: driverLicense.trim(),
+ licenseCategory: licenseCategory.trim(),
}),
})
@@ -509,29 +586,57 @@ export default function NewReservationPage() {
+
+
{copy.driverLicenseInfo}
diff --git a/apps/dashboard/src/components/reservations/VehicleConditionSheet.tsx b/apps/dashboard/src/components/reservations/VehicleConditionSheet.tsx
index 977fcd6..6ba10ca 100644
--- a/apps/dashboard/src/components/reservations/VehicleConditionSheet.tsx
+++ b/apps/dashboard/src/components/reservations/VehicleConditionSheet.tsx
@@ -11,7 +11,7 @@ export type VehicleSheetPoint = {
const SHEET_WIDTH = 573
const SHEET_HEIGHT = 876
-const SHEET_IMAGE_PATH = '/vehicle-condition-template.png'
+const SHEET_IMAGE_PATH = '/dashboard/vehicle-condition-template.png'
const severityColors: Record
= {
MINOR: '#f59e0b',
diff --git a/apps/marketplace/src/components/BookingForm.tsx b/apps/marketplace/src/components/BookingForm.tsx
index 8d8f411..73c6db6 100644
--- a/apps/marketplace/src/components/BookingForm.tsx
+++ b/apps/marketplace/src/components/BookingForm.tsx
@@ -16,10 +16,22 @@ type Props = {
const copy = {
en: {
title: 'Reserve this vehicle',
+ renterIdentity: 'Renter identity',
firstName: 'First name',
lastName: 'Last name',
+ dateOfBirth: 'Date of birth',
+ nationality: 'Nationality',
+ identityDocumentNumber: 'CIN / Passport number',
+ fullAddress: 'Full address',
+ driverLicenseSection: 'Driver license',
email: 'Email',
phone: 'Phone',
+ driverLicense: 'License number',
+ licenseIssuedAt: 'Issue date',
+ licenseExpiry: 'Expiry date',
+ licenseCountry: 'Country of issue',
+ licenseCategory: 'License category',
+ internationalLicenseNumber: 'International permit number (optional)',
pickupLocation: 'Pick-up location',
returnLocation: 'Drop-off location',
sameDropoff: 'Same drop-off',
@@ -42,10 +54,22 @@ const copy = {
},
fr: {
title: 'Réserver ce véhicule',
+ renterIdentity: 'Identité du locataire',
firstName: 'Prénom',
lastName: 'Nom',
+ dateOfBirth: 'Date de naissance',
+ nationality: 'Nationalité',
+ identityDocumentNumber: 'N° CIN / passeport',
+ fullAddress: 'Adresse complète',
+ driverLicenseSection: 'Permis de conduire',
email: 'E-mail',
phone: 'Téléphone',
+ driverLicense: 'Numéro du permis',
+ licenseIssuedAt: 'Date de délivrance',
+ licenseExpiry: 'Date d’expiration',
+ licenseCountry: 'Pays de délivrance',
+ licenseCategory: 'Catégorie du permis',
+ internationalLicenseNumber: 'N° de permis international (optionnel)',
pickupLocation: 'Lieu de départ',
returnLocation: 'Lieu de retour',
sameDropoff: 'Même retour',
@@ -68,10 +92,22 @@ const copy = {
},
ar: {
title: 'احجز هذه السيارة',
+ renterIdentity: 'بيانات المستأجر',
firstName: 'الاسم الأول',
lastName: 'اسم العائلة',
+ dateOfBirth: 'تاريخ الميلاد',
+ nationality: 'الجنسية',
+ identityDocumentNumber: 'رقم البطاقة الوطنية / جواز السفر',
+ fullAddress: 'العنوان الكامل',
+ driverLicenseSection: 'رخصة القيادة',
email: 'البريد الإلكتروني',
phone: 'الهاتف',
+ driverLicense: 'رقم الرخصة',
+ licenseIssuedAt: 'تاريخ الإصدار',
+ licenseExpiry: 'تاريخ الانتهاء',
+ licenseCountry: 'بلد الإصدار',
+ licenseCategory: 'فئة الرخصة',
+ internationalLicenseNumber: 'رقم الرخصة الدولية (اختياري)',
pickupLocation: 'موقع الاستلام',
returnLocation: 'موقع الإرجاع',
sameDropoff: 'نفس موقع الإرجاع',
@@ -110,8 +146,18 @@ export default function BookingForm({
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
+ const [dateOfBirth, setDateOfBirth] = useState('')
+ const [nationality, setNationality] = useState('')
+ const [identityDocumentNumber, setIdentityDocumentNumber] = useState('')
+ const [fullAddress, setFullAddress] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
+ const [driverLicense, setDriverLicense] = useState('')
+ const [licenseIssuedAt, setLicenseIssuedAt] = useState('')
+ const [licenseExpiry, setLicenseExpiry] = useState('')
+ const [licenseCountry, setLicenseCountry] = useState('')
+ const [licenseCategory, setLicenseCategory] = useState('')
+ const [internationalLicenseNumber, setInternationalLicenseNumber] = useState('')
const [pickupLocation, setPickupLocation] = useState(pickupOptions[0] ?? '')
const [dropoffMode, setDropoffMode] = useState<'same' | 'different'>('same')
const [returnLocation, setReturnLocation] = useState(dropoffOptions[0] ?? '')
@@ -131,7 +177,23 @@ export default function BookingForm({
e.preventDefault()
setError(null)
- if (!firstName.trim() || !lastName.trim() || !email.trim() || !phone.trim() || !startDate || !endDate) {
+ if (
+ !firstName.trim() ||
+ !lastName.trim() ||
+ !dateOfBirth ||
+ !nationality.trim() ||
+ !identityDocumentNumber.trim() ||
+ !fullAddress.trim() ||
+ !email.trim() ||
+ !phone.trim() ||
+ !driverLicense.trim() ||
+ !licenseIssuedAt ||
+ !licenseExpiry ||
+ !licenseCountry.trim() ||
+ !licenseCategory.trim() ||
+ !startDate ||
+ !endDate
+ ) {
setError(t.required)
return
}
@@ -154,8 +216,18 @@ export default function BookingForm({
companySlug,
firstName: firstName.trim(),
lastName: lastName.trim(),
+ dateOfBirth: new Date(dateOfBirth).toISOString(),
+ nationality: nationality.trim(),
+ identityDocumentNumber: identityDocumentNumber.trim(),
+ fullAddress: fullAddress.trim(),
email: email.trim(),
phone: phone.trim(),
+ driverLicense: driverLicense.trim(),
+ licenseIssuedAt: new Date(licenseIssuedAt).toISOString(),
+ licenseExpiry: new Date(licenseExpiry).toISOString(),
+ licenseCountry: licenseCountry.trim(),
+ licenseCategory: licenseCategory.trim(),
+ internationalLicenseNumber: internationalLicenseNumber.trim() || undefined,
startDate: start.toISOString(),
endDate: end.toISOString(),
pickupLocation: pickupLocation || undefined,
@@ -190,22 +262,68 @@ export default function BookingForm({
) : null}
-
+
+
{t.renterIdentity}
+
+
+
+
-
+
setFirstName(e.target.value)}
+ value={identityDocumentNumber}
+ onChange={(e) => setIdentityDocumentNumber(e.target.value)}
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
/>
+
-
- {t.fullAddress} *
+
@@ -222,6 +340,72 @@ export default function BookingForm({
/>
+
+
{t.driverLicenseSection}
+
+
+ setDriverLicense(e.target.value)}
+ className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
+ />
+
+
+
+
+
+
+
+
+ setInternationalLicenseNumber(e.target.value)}
+ className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 outline-none focus:border-stone-400 dark:border-blue-800 dark:bg-[#162038] dark:text-stone-100 dark:focus:border-stone-500"
+ />
+
+
+