diff --git a/apps/api/package.json b/apps/api/package.json
index 83fb984..9dd4d78 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -41,8 +41,10 @@
"react-dom": "^18.3.1",
"resend": "^3.2.0",
"socket.io": "^4.7.5",
+ "swagger-ui-express": "^5.0.1",
"twilio": "^5.1.0",
- "zod": "^3.23.0"
+ "zod": "^3.23.0",
+ "zod-to-json-schema": "^3.25.2"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
@@ -58,6 +60,7 @@
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/supertest": "^6.0.2",
+ "@types/swagger-ui-express": "^4.1.8",
"supertest": "^7.0.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.0",
diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts
index 70f6b59..55f1a4e 100644
--- a/apps/api/src/app.ts
+++ b/apps/api/src/app.ts
@@ -2,6 +2,8 @@ import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import morgan from 'morgan'
+import swaggerUi from 'swagger-ui-express'
+import { openApiDocument } from './swagger/openapi'
import { getLegacyStorageRoots, getStorageRoot } from './lib/storage'
import { authLimiter, apiLimiter, publicLimiter, adminLimiter } from './middleware/rateLimiter'
@@ -100,6 +102,10 @@ export function createApp() {
app.use('/storage', express.static(legacyRoot))
}
+ // Swagger UI — mounted before helmet so its assets are not blocked by CSP
+ app.use('/docs', swaggerUi.serve, swaggerUi.setup(openApiDocument, { customSiteTitle: 'RentalDriveGo API Docs' }))
+ app.get('/api/v1/openapi.json', (_req, res) => res.json(openApiDocument))
+
// Webhook must use raw body BEFORE express.json()
app.use('/api/v1/webhooks', express.raw({ type: 'application/json' }), webhookRouter)
@@ -141,14 +147,14 @@ export function createApp() {
})
app.get(`${v1}/docs`, (_req, res) => {
- res.json({ name: 'rentaldrivego-api', version: '1.0.0', baseUrl: v1, docsUrl: '/docs', routes: routeDocs })
- })
-
- app.get('/docs', (_req, res) => {
- const rows = routeDocs
- .map((r) => `
| ${r.method} | ${r.path} | ${r.description} |
`)
- .join('')
- res.type('html').send(`RentalDriveGo API DocsRentalDriveGo API
| Method | Path | Description |
${rows}
`)
+ res.json({
+ name: 'rentaldrivego-api',
+ version: '1.0.0',
+ baseUrl: v1,
+ routes: routeDocs,
+ swaggerUi: '/docs',
+ openApi: '/api/v1/openapi.json',
+ })
})
// ─── Error handler ──────────────────────────────────────────
diff --git a/apps/api/src/swagger/openapi.ts b/apps/api/src/swagger/openapi.ts
new file mode 100644
index 0000000..33c0b2d
--- /dev/null
+++ b/apps/api/src/swagger/openapi.ts
@@ -0,0 +1,1146 @@
+import { zodToJsonSchema } from 'zod-to-json-schema'
+
+// ── Vehicles ─────────────────────────────────────────────────────────────────
+import {
+ vehicleSchema,
+ calendarBlockSchema,
+ maintenanceLogSchema,
+ publishSchema,
+ statusSchema,
+} from '../modules/vehicles/vehicle.schemas'
+
+// ── Reservations ─────────────────────────────────────────────────────────────
+import {
+ createSchema as reservationCreateSchema,
+ updateSchema as reservationUpdateSchema,
+ checkinSchema,
+ checkoutSchema,
+ extendSchema,
+ cancelSchema,
+ approvalSchema,
+ inspectionSchema,
+ additionalDriverSchema,
+} from '../modules/reservations/reservation.schemas'
+
+// ── Customers ─────────────────────────────────────────────────────────────────
+import {
+ customerSchema,
+ approveLicenseSchema,
+ flagSchema,
+} from '../modules/customers/customer.schemas'
+
+// ── Offers ────────────────────────────────────────────────────────────────────
+import { offerSchema } from '../modules/offers/offer.schemas'
+
+// ── Payments ─────────────────────────────────────────────────────────────────
+import {
+ chargeSchema,
+ manualPaymentSchema,
+ refundSchema,
+} from '../modules/payments/payment.schemas'
+
+// ── Reviews ───────────────────────────────────────────────────────────────────
+import { replySchema } from '../modules/reviews/review.schemas'
+
+// ── Complaints ────────────────────────────────────────────────────────────────
+import {
+ createSchema as complaintCreateSchema,
+ updateSchema as complaintUpdateSchema,
+} from '../modules/complaints/complaint.schemas'
+
+// ── Auth ──────────────────────────────────────────────────────────────────────
+import { companySignupSchema } from '../modules/auth/auth.company.schemas'
+import {
+ employeeLoginSchema,
+ employeeForgotPasswordSchema,
+ employeeResetPasswordSchema,
+ employeeLanguageSchema,
+} from '../modules/auth/auth.employee.schemas'
+import { renterUpdateSchema, renterFcmTokenSchema } from '../modules/auth/auth.renter.schemas'
+
+// ─────────────────────────────────────────────────────────────────────────────
+
+function s(schema: Parameters[0]): Record {
+ const { $schema, ...rest } = zodToJsonSchema(schema, { target: 'openApi3' }) as Record
+ return rest
+}
+
+const idPath = (name = 'id') => ({
+ name,
+ in: 'path' as const,
+ required: true,
+ schema: { type: 'string' },
+})
+
+const pagingParams = [
+ { name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },
+ { name: 'pageSize', in: 'query', schema: { type: 'integer', default: 20 } },
+]
+
+const jsonBody = (ref: string) => ({
+ required: true,
+ content: { 'application/json': { schema: { $ref: ref } } },
+})
+
+const ok = { description: 'Success' }
+const err4 = { description: 'Validation error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
+const err401 = { description: 'Unauthorized' }
+const err404 = { description: 'Not found' }
+
+export const openApiDocument = {
+ openapi: '3.0.3',
+ info: {
+ title: 'RentalDriveGo API',
+ description: 'REST API for the RentalDriveGo car rental management platform.\n\n**Auth:** Most endpoints require a Bearer JWT. Obtain a token via `/auth/employee/login` (dashboard/admin users) or via Firebase for renters.',
+ version: '1.0.0',
+ contact: { name: 'RentalDriveGo', email: 'support@rentaldrivego.com' },
+ },
+ servers: [{ url: '/api/v1', description: 'v1' }],
+ security: [{ bearerAuth: [] }],
+ tags: [
+ { name: 'Health', description: 'Service health' },
+ { name: 'Auth — Company', description: 'Company account registration' },
+ { name: 'Auth — Employee', description: 'Employee (dashboard) authentication' },
+ { name: 'Auth — Renter', description: 'Renter (mobile) authentication' },
+ { name: 'Vehicles', description: 'Fleet management' },
+ { name: 'Reservations', description: 'Booking lifecycle' },
+ { name: 'Customers', description: 'Customer profiles & documents' },
+ { name: 'Offers', description: 'Discounts & promo codes' },
+ { name: 'Payments', description: 'Charges, manual payments, refunds' },
+ { name: 'Reviews', description: 'Customer reviews' },
+ { name: 'Complaints', description: 'Complaint tracking' },
+ { name: 'Analytics', description: 'Dashboard & reports' },
+ { name: 'Notifications', description: 'In-app notifications' },
+ { name: 'Companies', description: 'Company profile & settings' },
+ { name: 'Team', description: 'Employee management' },
+ { name: 'Subscriptions', description: 'Plan management & billing' },
+ { name: 'Marketplace', description: 'Public booking portal' },
+ { name: 'Site', description: 'White-label site API' },
+ { name: 'Admin', description: 'Platform administration' },
+ ],
+ components: {
+ securitySchemes: {
+ bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
+ },
+ schemas: {
+ // ── Vehicles ────────────────────────────────────────────────
+ VehicleInput: s(vehicleSchema),
+ CalendarBlock: s(calendarBlockSchema),
+ MaintenanceLog: s(maintenanceLogSchema),
+ VehiclePublish: s(publishSchema),
+ VehicleStatus: s(statusSchema),
+ // ── Reservations ────────────────────────────────────────────
+ ReservationCreate: s(reservationCreateSchema),
+ ReservationUpdate: s(reservationUpdateSchema),
+ AdditionalDriver: s(additionalDriverSchema),
+ Inspection: s(inspectionSchema),
+ ReservationCheckin: s(checkinSchema),
+ ReservationCheckout:s(checkoutSchema),
+ ReservationExtend: s(extendSchema),
+ ReservationCancel: s(cancelSchema),
+ ReservationApproval:s(approvalSchema),
+ // ── Customers ───────────────────────────────────────────────
+ CustomerInput: s(customerSchema),
+ ApproveLicense: s(approveLicenseSchema),
+ FlagCustomer: s(flagSchema),
+ // ── Offers ──────────────────────────────────────────────────
+ OfferInput: s(offerSchema),
+ // ── Payments ────────────────────────────────────────────────
+ PaymentCharge: s(chargeSchema),
+ ManualPayment: s(manualPaymentSchema),
+ Refund: s(refundSchema),
+ // ── Reviews ─────────────────────────────────────────────────
+ ReviewReply: s(replySchema),
+ // ── Complaints ──────────────────────────────────────────────
+ ComplaintCreate: s(complaintCreateSchema),
+ ComplaintUpdate: s(complaintUpdateSchema),
+ // ── Auth ────────────────────────────────────────────────────
+ CompanySignup: s(companySignupSchema),
+ EmployeeLogin: s(employeeLoginSchema),
+ EmployeeForgotPassword: s(employeeForgotPasswordSchema),
+ EmployeeResetPassword: s(employeeResetPasswordSchema),
+ EmployeeLanguage: s(employeeLanguageSchema),
+ RenterUpdate: s(renterUpdateSchema),
+ RenterFcmToken: s(renterFcmTokenSchema),
+ },
+ },
+ paths: {
+ // ════════════════════════════════════════════════════════════════
+ // HEALTH (no /api/v1 prefix — served directly from app root)
+ // ════════════════════════════════════════════════════════════════
+ '/health': {
+ get: {
+ tags: ['Health'],
+ summary: 'Health check',
+ security: [],
+ servers: [{ url: '/' }],
+ responses: {
+ '200': { description: 'OK', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', example: 'ok' }, version: { type: 'string' }, timestamp: { type: 'string', format: 'date-time' } } } } } },
+ },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // AUTH — COMPANY
+ // ════════════════════════════════════════════════════════════════
+ '/auth/company/signup': {
+ post: {
+ tags: ['Auth — Company'],
+ summary: 'Register a new company',
+ security: [],
+ requestBody: jsonBody('#/components/schemas/CompanySignup'),
+ responses: { '201': ok, '400': err4, '409': { description: 'Email already in use' } },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // AUTH — EMPLOYEE
+ // ════════════════════════════════════════════════════════════════
+ '/auth/employee/login': {
+ post: {
+ tags: ['Auth — Employee'],
+ summary: 'Login',
+ security: [],
+ requestBody: jsonBody('#/components/schemas/EmployeeLogin'),
+ responses: {
+ '200': { description: 'Token + employee object', content: { 'application/json': { schema: { type: 'object', properties: { token: { type: 'string' }, employee: { type: 'object' } } } } } },
+ '401': err401,
+ },
+ },
+ },
+ '/auth/employee/forgot-password': {
+ post: {
+ tags: ['Auth — Employee'],
+ summary: 'Request password-reset email',
+ security: [],
+ requestBody: jsonBody('#/components/schemas/EmployeeForgotPassword'),
+ responses: { '200': ok },
+ },
+ },
+ '/auth/employee/reset-password': {
+ post: {
+ tags: ['Auth — Employee'],
+ summary: 'Reset password with token',
+ security: [],
+ requestBody: jsonBody('#/components/schemas/EmployeeResetPassword'),
+ responses: { '200': ok, '400': err4 },
+ },
+ },
+ '/auth/employee/me': {
+ get: {
+ tags: ['Auth — Employee'],
+ summary: 'Current employee profile',
+ responses: { '200': ok, '401': err401 },
+ },
+ },
+ '/auth/employee/me/language': {
+ patch: {
+ tags: ['Auth — Employee'],
+ summary: 'Update preferred language',
+ requestBody: jsonBody('#/components/schemas/EmployeeLanguage'),
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // AUTH — RENTER
+ // ════════════════════════════════════════════════════════════════
+ '/auth/renter/me': {
+ get: {
+ tags: ['Auth — Renter'],
+ summary: 'Current renter profile',
+ responses: { '200': ok, '401': err401 },
+ },
+ patch: {
+ tags: ['Auth — Renter'],
+ summary: 'Update renter profile',
+ requestBody: jsonBody('#/components/schemas/RenterUpdate'),
+ responses: { '200': ok },
+ },
+ },
+ '/auth/renter/me/fcm-token': {
+ post: {
+ tags: ['Auth — Renter'],
+ summary: 'Register FCM push token',
+ requestBody: jsonBody('#/components/schemas/RenterFcmToken'),
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // VEHICLES
+ // ════════════════════════════════════════════════════════════════
+ '/vehicles': {
+ get: {
+ tags: ['Vehicles'],
+ summary: 'List vehicles',
+ parameters: [
+ { name: 'status', in: 'query', schema: { type: 'string', enum: ['AVAILABLE','RESERVED','READY','RENTED','RETURNED','NEEDS_CLEANING','MAINTENANCE','DAMAGE_REVIEW','OUT_OF_SERVICE'] } },
+ { name: 'category', in: 'query', schema: { type: 'string', enum: ['ECONOMY','COMPACT','MIDSIZE','FULLSIZE','SUV','LUXURY','VAN','TRUCK'] } },
+ { name: 'published', in: 'query', schema: { type: 'string', enum: ['true','false'] } },
+ ...pagingParams,
+ ],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Vehicles'],
+ summary: 'Create vehicle',
+ requestBody: jsonBody('#/components/schemas/VehicleInput'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/vehicles/{id}': {
+ get: {
+ tags: ['Vehicles'],
+ summary: 'Get vehicle',
+ parameters: [idPath()],
+ responses: { '200': ok, '404': err404 },
+ },
+ patch: {
+ tags: ['Vehicles'],
+ summary: 'Update vehicle',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/VehicleInput'),
+ responses: { '200': ok, '404': err404 },
+ },
+ delete: {
+ tags: ['Vehicles'],
+ summary: 'Delete vehicle',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/status': {
+ patch: {
+ tags: ['Vehicles'],
+ summary: 'Set vehicle status',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/VehicleStatus'),
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/publish': {
+ patch: {
+ tags: ['Vehicles'],
+ summary: 'Publish / unpublish vehicle',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/VehiclePublish'),
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/photos': {
+ post: {
+ tags: ['Vehicles'],
+ summary: 'Upload photos (multipart, max 10)',
+ parameters: [idPath()],
+ requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { photos: { type: 'array', items: { type: 'string', format: 'binary' } } } } } } },
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/photos/{idx}': {
+ delete: {
+ tags: ['Vehicles'],
+ summary: 'Delete photo by index',
+ parameters: [idPath(), { name: 'idx', in: 'path', required: true, schema: { type: 'integer' } }],
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/availability': {
+ get: {
+ tags: ['Vehicles'],
+ summary: 'Check availability for a date range',
+ parameters: [
+ idPath(),
+ { name: 'startDate', in: 'query', required: true, schema: { type: 'string', format: 'date-time' } },
+ { name: 'endDate', in: 'query', required: true, schema: { type: 'string', format: 'date-time' } },
+ ],
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/calendar': {
+ get: {
+ tags: ['Vehicles'],
+ summary: 'Get monthly calendar events',
+ parameters: [
+ idPath(),
+ { name: 'year', in: 'query', required: true, schema: { type: 'integer' } },
+ { name: 'month', in: 'query', required: true, schema: { type: 'integer', minimum: 1, maximum: 12 } },
+ ],
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/calendar/blocks': {
+ post: {
+ tags: ['Vehicles'],
+ summary: 'Create calendar block',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/CalendarBlock'),
+ responses: { '201': ok },
+ },
+ },
+ '/vehicles/{id}/calendar/blocks/{blockId}': {
+ delete: {
+ tags: ['Vehicles'],
+ summary: 'Delete calendar block',
+ parameters: [idPath(), idPath('blockId')],
+ responses: { '200': ok },
+ },
+ },
+ '/vehicles/{id}/maintenance': {
+ get: {
+ tags: ['Vehicles'],
+ summary: 'List maintenance logs',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Vehicles'],
+ summary: 'Add maintenance log',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/MaintenanceLog'),
+ responses: { '201': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // RESERVATIONS
+ // ════════════════════════════════════════════════════════════════
+ '/reservations': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'List reservations',
+ parameters: [
+ { name: 'status', in: 'query', schema: { type: 'string' } },
+ { name: 'vehicleId', in: 'query', schema: { type: 'string' } },
+ { name: 'source', in: 'query', schema: { type: 'string' } },
+ { name: 'startDate', in: 'query', schema: { type: 'string' } },
+ { name: 'endDate', in: 'query', schema: { type: 'string' } },
+ ...pagingParams,
+ ],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Reservations'],
+ summary: 'Create reservation',
+ requestBody: jsonBody('#/components/schemas/ReservationCreate'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/reservations/{id}': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'Get reservation',
+ parameters: [idPath()],
+ responses: { '200': ok, '404': err404 },
+ },
+ patch: {
+ tags: ['Reservations'],
+ summary: 'Update reservation',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationUpdate'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/confirm': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Confirm reservation',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/approve': {
+ patch: {
+ tags: ['Reservations'],
+ summary: 'Approve or reject pending reservation',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationApproval'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/checkin': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Check in',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationCheckin'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/checkout': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Check out',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationCheckout'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/close': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Close reservation',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/extend': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Extend reservation end date',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationExtend'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/cancel': {
+ post: {
+ tags: ['Reservations'],
+ summary: 'Cancel reservation',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReservationCancel'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/contract': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'Download rental contract (PDF)',
+ parameters: [idPath()],
+ responses: { '200': { description: 'PDF file', content: { 'application/pdf': {} } } },
+ },
+ },
+ '/reservations/{id}/billing': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'Get billing summary',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/inspections': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'List inspections',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/inspections/{type}': {
+ put: {
+ tags: ['Reservations'],
+ summary: 'Save inspection (CHECKIN or CHECKOUT)',
+ parameters: [
+ idPath(),
+ { name: 'type', in: 'path', required: true, schema: { type: 'string', enum: ['CHECKIN', 'CHECKOUT'] } },
+ ],
+ requestBody: jsonBody('#/components/schemas/Inspection'),
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/photos': {
+ get: {
+ tags: ['Reservations'],
+ summary: 'List reservation photos',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Reservations'],
+ summary: 'Upload reservation photo',
+ parameters: [idPath()],
+ requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { photo: { type: 'string', format: 'binary' } } } } } },
+ responses: { '201': ok },
+ },
+ },
+ '/reservations/{id}/photos/{photoId}': {
+ delete: {
+ tags: ['Reservations'],
+ summary: 'Delete reservation photo',
+ parameters: [idPath(), idPath('photoId')],
+ responses: { '200': ok },
+ },
+ },
+ '/reservations/{id}/additional-drivers/{driverId}/approval': {
+ patch: {
+ tags: ['Reservations'],
+ summary: 'Approve/reject additional driver',
+ parameters: [idPath(), idPath('driverId')],
+ requestBody: jsonBody('#/components/schemas/ReservationApproval'),
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // CUSTOMERS
+ // ════════════════════════════════════════════════════════════════
+ '/customers': {
+ get: {
+ tags: ['Customers'],
+ summary: 'List customers',
+ parameters: [
+ { name: 'q', in: 'query', schema: { type: 'string' } },
+ { name: 'flagged', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } },
+ ...pagingParams,
+ ],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Customers'],
+ summary: 'Create customer',
+ requestBody: jsonBody('#/components/schemas/CustomerInput'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/customers/{id}': {
+ get: {
+ tags: ['Customers'],
+ summary: 'Get customer',
+ parameters: [idPath()],
+ responses: { '200': ok, '404': err404 },
+ },
+ patch: {
+ tags: ['Customers'],
+ summary: 'Update customer',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/CustomerInput'),
+ responses: { '200': ok },
+ },
+ },
+ '/customers/{id}/flag': {
+ post: {
+ tags: ['Customers'],
+ summary: 'Flag customer',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/FlagCustomer'),
+ responses: { '200': ok },
+ },
+ delete: {
+ tags: ['Customers'],
+ summary: 'Unflag customer',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/customers/{id}/validate-license': {
+ post: {
+ tags: ['Customers'],
+ summary: 'Validate customer driving license',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/customers/{id}/license-image': {
+ post: {
+ tags: ['Customers'],
+ summary: 'Upload license image',
+ parameters: [idPath()],
+ requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } },
+ responses: { '200': ok },
+ },
+ },
+ '/customers/{id}/approve-license': {
+ post: {
+ tags: ['Customers'],
+ summary: 'Approve or deny license',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ApproveLicense'),
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // OFFERS
+ // ════════════════════════════════════════════════════════════════
+ '/offers': {
+ get: {
+ tags: ['Offers'],
+ summary: 'List offers',
+ parameters: [
+ { name: 'active', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } },
+ { name: 'public', in: 'query', schema: { type: 'string', enum: ['true', 'false'] } },
+ ],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Offers'],
+ summary: 'Create offer',
+ requestBody: jsonBody('#/components/schemas/OfferInput'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/offers/{id}': {
+ get: {
+ tags: ['Offers'],
+ summary: 'Get offer',
+ parameters: [idPath()],
+ responses: { '200': ok, '404': err404 },
+ },
+ patch: {
+ tags: ['Offers'],
+ summary: 'Update offer',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/OfferInput'),
+ responses: { '200': ok },
+ },
+ delete: {
+ tags: ['Offers'],
+ summary: 'Delete offer',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/offers/{id}/activate': {
+ post: { tags: ['Offers'], summary: 'Activate offer', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/offers/{id}/deactivate': {
+ post: { tags: ['Offers'], summary: 'Deactivate offer', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/offers/{id}/stats': {
+ get: { tags: ['Offers'], summary: 'Offer usage stats', parameters: [idPath()], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // PAYMENTS
+ // ════════════════════════════════════════════════════════════════
+ '/payments/company': {
+ get: {
+ tags: ['Payments'],
+ summary: 'List all company payments',
+ responses: { '200': ok },
+ },
+ },
+ '/payments/reservations/{id}': {
+ get: {
+ tags: ['Payments'],
+ summary: 'Get payments for a reservation',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/payments/reservations/{id}/charge': {
+ post: {
+ tags: ['Payments'],
+ summary: 'Initiate online payment (AmanPay / PayPal)',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/PaymentCharge'),
+ responses: { '200': ok, '400': err4 },
+ },
+ },
+ '/payments/reservations/{id}/capture-paypal': {
+ post: {
+ tags: ['Payments'],
+ summary: 'Capture PayPal order',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+ '/payments/reservations/{id}/manual': {
+ post: {
+ tags: ['Payments'],
+ summary: 'Record manual payment',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ManualPayment'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/payments/reservations/{reservationId}/payments/{paymentId}/refund': {
+ post: {
+ tags: ['Payments'],
+ summary: 'Refund a payment',
+ parameters: [idPath('reservationId'), idPath('paymentId')],
+ requestBody: jsonBody('#/components/schemas/Refund'),
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // REVIEWS
+ // ════════════════════════════════════════════════════════════════
+ '/reviews': {
+ get: {
+ tags: ['Reviews'],
+ summary: 'List reviews',
+ parameters: [
+ { name: 'rating', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 5 } },
+ ...pagingParams,
+ ],
+ responses: { '200': ok },
+ },
+ },
+ '/reviews/stats': {
+ get: { tags: ['Reviews'], summary: 'Review statistics', responses: { '200': ok } },
+ },
+ '/reviews/{id}': {
+ get: { tags: ['Reviews'], summary: 'Get review', parameters: [idPath()], responses: { '200': ok, '404': err404 } },
+ },
+ '/reviews/{id}/reply': {
+ patch: {
+ tags: ['Reviews'],
+ summary: 'Reply to review',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ReviewReply'),
+ responses: { '200': ok },
+ },
+ },
+ '/reviews/{id}/remind': {
+ post: { tags: ['Reviews'], summary: 'Send review reminder', parameters: [idPath()], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // COMPLAINTS
+ // ════════════════════════════════════════════════════════════════
+ '/complaints': {
+ get: {
+ tags: ['Complaints'],
+ summary: 'List complaints',
+ parameters: [
+ { name: 'status', in: 'query', schema: { type: 'string' } },
+ { name: 'severity', in: 'query', schema: { type: 'string' } },
+ { name: 'category', in: 'query', schema: { type: 'string' } },
+ ...pagingParams,
+ ],
+ responses: { '200': ok },
+ },
+ post: {
+ tags: ['Complaints'],
+ summary: 'Create complaint',
+ requestBody: jsonBody('#/components/schemas/ComplaintCreate'),
+ responses: { '201': ok, '400': err4 },
+ },
+ },
+ '/complaints/{id}': {
+ get: {
+ tags: ['Complaints'],
+ summary: 'Get complaint',
+ parameters: [idPath()],
+ responses: { '200': ok, '404': err404 },
+ },
+ patch: {
+ tags: ['Complaints'],
+ summary: 'Update complaint',
+ parameters: [idPath()],
+ requestBody: jsonBody('#/components/schemas/ComplaintUpdate'),
+ responses: { '200': ok },
+ },
+ delete: {
+ tags: ['Complaints'],
+ summary: 'Delete complaint',
+ parameters: [idPath()],
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // ANALYTICS
+ // ════════════════════════════════════════════════════════════════
+ '/analytics/summary': {
+ get: { tags: ['Analytics'], summary: 'Quick summary stats', responses: { '200': ok } },
+ },
+ '/analytics/dashboard': {
+ get: { tags: ['Analytics'], summary: 'Full dashboard analytics', responses: { '200': ok } },
+ },
+ '/analytics/sources': {
+ get: { tags: ['Analytics'], summary: 'Reservation source breakdown', responses: { '200': ok } },
+ },
+ '/analytics/report': {
+ get: {
+ tags: ['Analytics'],
+ summary: 'Detailed report (Manager+)',
+ parameters: [
+ { name: 'startDate', in: 'query', schema: { type: 'string', format: 'date' } },
+ { name: 'endDate', in: 'query', schema: { type: 'string', format: 'date' } },
+ ],
+ responses: { '200': ok },
+ },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // NOTIFICATIONS
+ // ════════════════════════════════════════════════════════════════
+ '/notifications/company': {
+ get: { tags: ['Notifications'], summary: 'Company notifications', responses: { '200': ok } },
+ },
+ '/notifications/unread-count': {
+ get: { tags: ['Notifications'], summary: 'Unread count', responses: { '200': ok } },
+ },
+ '/notifications/company/{id}/read': {
+ post: { tags: ['Notifications'], summary: 'Mark notification as read', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/notifications/company/read-all': {
+ post: { tags: ['Notifications'], summary: 'Mark all notifications as read', responses: { '200': ok } },
+ },
+ '/notifications/company/preferences': {
+ get: { tags: ['Notifications'], summary: 'Get notification preferences', responses: { '200': ok } },
+ patch: { tags: ['Notifications'], summary: 'Update notification preferences', responses: { '200': ok } },
+ },
+ '/notifications/history': {
+ get: { tags: ['Notifications'], summary: 'Notification history', responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // COMPANIES
+ // ════════════════════════════════════════════════════════════════
+ '/companies/me': {
+ get: { tags: ['Companies'], summary: 'Current company profile', responses: { '200': ok } },
+ patch: { tags: ['Companies'], summary: 'Update company profile (Owner)', responses: { '200': ok } },
+ },
+ '/companies/me/brand': {
+ get: { tags: ['Companies'], summary: 'Brand settings', responses: { '200': ok } },
+ patch: { tags: ['Companies'], summary: 'Update brand settings (Owner)', responses: { '200': ok } },
+ },
+ '/companies/me/brand/logo': {
+ post: { tags: ['Companies'], summary: 'Upload brand logo', requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } }, responses: { '200': ok } },
+ },
+ '/companies/me/brand/hero': {
+ post: { tags: ['Companies'], summary: 'Upload brand hero image', requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } } } } }, responses: { '200': ok } },
+ },
+ '/companies/me/brand/subdomain/check': {
+ post: { tags: ['Companies'], summary: 'Check subdomain availability', responses: { '200': ok } },
+ },
+ '/companies/me/brand/custom-domain': {
+ post: { tags: ['Companies'], summary: 'Set custom domain', responses: { '200': ok } },
+ delete: { tags: ['Companies'], summary: 'Remove custom domain', responses: { '200': ok } },
+ },
+ '/companies/me/brand/custom-domain/status': {
+ get: { tags: ['Companies'], summary: 'Custom domain DNS status', responses: { '200': ok } },
+ },
+ '/companies/me/contract-settings': {
+ get: { tags: ['Companies'], summary: 'Contract settings', responses: { '200': ok } },
+ patch: { tags: ['Companies'], summary: 'Update contract settings', responses: { '200': ok } },
+ },
+ '/companies/me/insurance-policies': {
+ get: { tags: ['Companies'], summary: 'List insurance policies', responses: { '200': ok } },
+ post: { tags: ['Companies'], summary: 'Create insurance policy', responses: { '201': ok } },
+ },
+ '/companies/me/insurance-policies/{id}': {
+ patch: { tags: ['Companies'], summary: 'Update insurance policy', parameters: [idPath()], responses: { '200': ok } },
+ delete: { tags: ['Companies'], summary: 'Delete insurance policy', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/companies/me/pricing-rules': {
+ get: { tags: ['Companies'], summary: 'List pricing rules', responses: { '200': ok } },
+ post: { tags: ['Companies'], summary: 'Create pricing rule', responses: { '201': ok } },
+ },
+ '/companies/me/pricing-rules/{id}': {
+ patch: { tags: ['Companies'], summary: 'Update pricing rule', parameters: [idPath()], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // TEAM
+ // ════════════════════════════════════════════════════════════════
+ '/team': {
+ get: { tags: ['Team'], summary: 'List team members', responses: { '200': ok } },
+ },
+ '/team/stats': {
+ get: { tags: ['Team'], summary: 'Team statistics', responses: { '200': ok } },
+ },
+ '/team/invite': {
+ post: { tags: ['Team'], summary: 'Invite member (Owner)', responses: { '201': ok } },
+ },
+ '/team/{id}/role': {
+ patch: { tags: ['Team'], summary: 'Update member role (Owner)', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/team/{id}/deactivate': {
+ post: { tags: ['Team'], summary: 'Deactivate member (Owner)', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/team/{id}/reactivate': {
+ post: { tags: ['Team'], summary: 'Reactivate member (Owner)', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/team/{id}': {
+ delete: { tags: ['Team'], summary: 'Remove member (Owner)', parameters: [idPath()], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // SUBSCRIPTIONS
+ // ════════════════════════════════════════════════════════════════
+ '/subscriptions/plans': {
+ get: { tags: ['Subscriptions'], summary: 'List available plans', security: [], responses: { '200': ok } },
+ },
+ '/subscriptions/features': {
+ get: { tags: ['Subscriptions'], summary: 'Plan feature matrix', security: [], responses: { '200': ok } },
+ },
+ '/subscriptions/me': {
+ get: { tags: ['Subscriptions'], summary: 'Current subscription', responses: { '200': ok } },
+ },
+ '/subscriptions/invoices': {
+ get: { tags: ['Subscriptions'], summary: 'Billing invoices', responses: { '200': ok } },
+ },
+ '/subscriptions/events': {
+ get: { tags: ['Subscriptions'], summary: 'Subscription events', responses: { '200': ok } },
+ },
+ '/subscriptions/entitlement': {
+ get: { tags: ['Subscriptions'], summary: 'Feature entitlements', responses: { '200': ok } },
+ },
+ '/subscriptions/trial': {
+ post: { tags: ['Subscriptions'], summary: 'Start trial (Owner)', responses: { '200': ok } },
+ },
+ '/subscriptions/checkout': {
+ post: { tags: ['Subscriptions'], summary: 'Checkout new subscription (Owner)', responses: { '200': ok } },
+ },
+ '/subscriptions/change-plan': {
+ post: { tags: ['Subscriptions'], summary: 'Change plan (Owner)', responses: { '200': ok } },
+ },
+ '/subscriptions/cancel': {
+ post: { tags: ['Subscriptions'], summary: 'Cancel subscription (Owner)', responses: { '200': ok } },
+ },
+ '/subscriptions/reactivate': {
+ post: { tags: ['Subscriptions'], summary: 'Reactivate subscription (Owner)', responses: { '200': ok } },
+ },
+ '/subscriptions/resume': {
+ post: { tags: ['Subscriptions'], summary: 'Resume subscription (Owner)', responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // MARKETPLACE (public — no auth required)
+ // ════════════════════════════════════════════════════════════════
+ '/marketplace/cities': {
+ get: { tags: ['Marketplace'], summary: 'Cities with active companies', security: [], responses: { '200': ok } },
+ },
+ '/marketplace/companies': {
+ get: { tags: ['Marketplace'], summary: 'List companies', security: [], responses: { '200': ok } },
+ },
+ '/marketplace/offers': {
+ get: { tags: ['Marketplace'], summary: 'Public offers', security: [], responses: { '200': ok } },
+ },
+ '/marketplace/search': {
+ get: {
+ tags: ['Marketplace'],
+ summary: 'Search available vehicles',
+ security: [],
+ parameters: [
+ { name: 'city', in: 'query', schema: { type: 'string' } },
+ { name: 'startDate', in: 'query', schema: { type: 'string', format: 'date-time' } },
+ { name: 'endDate', in: 'query', schema: { type: 'string', format: 'date-time' } },
+ { name: 'category', in: 'query', schema: { type: 'string' } },
+ ],
+ responses: { '200': ok },
+ },
+ },
+ '/marketplace/reservations': {
+ post: { tags: ['Marketplace'], summary: 'Create marketplace booking', security: [], responses: { '201': ok } },
+ },
+ '/marketplace/offers/{code}/validate': {
+ post: { tags: ['Marketplace'], summary: 'Validate promo code', security: [], parameters: [{ name: 'code', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/marketplace/review/{token}': {
+ get: { tags: ['Marketplace'], summary: 'Get review form by token', security: [], parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ post: { tags: ['Marketplace'], summary: 'Submit review', security: [], parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/marketplace/{slug}': {
+ get: { tags: ['Marketplace'], summary: 'Company public page', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/marketplace/{slug}/reviews': {
+ get: { tags: ['Marketplace'], summary: 'Company reviews', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/marketplace/{slug}/vehicles': {
+ get: { tags: ['Marketplace'], summary: 'Company vehicles', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/marketplace/{slug}/vehicles/{id}': {
+ get: { tags: ['Marketplace'], summary: 'Vehicle details', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } },
+ },
+ '/marketplace/{slug}/offers': {
+ get: { tags: ['Marketplace'], summary: 'Company offers', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // SITE (white-label)
+ // ════════════════════════════════════════════════════════════════
+ '/site/platform/homepage': {
+ get: { tags: ['Site'], summary: 'Platform homepage content', security: [], responses: { '200': ok } },
+ },
+ '/site/platform/pricing': {
+ get: { tags: ['Site'], summary: 'Platform pricing page content', security: [], responses: { '200': ok } },
+ },
+ '/site/{slug}/brand': {
+ get: { tags: ['Site'], summary: 'Brand config for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/vehicles': {
+ get: { tags: ['Site'], summary: 'Vehicles for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/vehicles/{id}': {
+ get: { tags: ['Site'], summary: 'Vehicle detail', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } },
+ },
+ '/site/{slug}/offers': {
+ get: { tags: ['Site'], summary: 'Offers for white-label site', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/booking-options': {
+ get: { tags: ['Site'], summary: 'Booking options', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/availability': {
+ post: { tags: ['Site'], summary: 'Check availability', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/book/validate-code': {
+ post: { tags: ['Site'], summary: 'Validate promo code', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+ '/site/{slug}/book': {
+ post: { tags: ['Site'], summary: 'Create booking', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '201': ok } },
+ },
+ '/site/{slug}/booking/{id}': {
+ get: { tags: ['Site'], summary: 'Get booking status', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } },
+ },
+ '/site/{slug}/booking/{id}/pay': {
+ post: { tags: ['Site'], summary: 'Pay booking', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }, idPath()], responses: { '200': ok } },
+ },
+ '/site/{slug}/contact': {
+ post: { tags: ['Site'], summary: 'Contact form', security: [], parameters: [{ name: 'slug', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': ok } },
+ },
+
+ // ════════════════════════════════════════════════════════════════
+ // ADMIN
+ // ════════════════════════════════════════════════════════════════
+ '/admin/auth/login': {
+ post: { tags: ['Admin'], summary: 'Admin login', security: [], responses: { '200': ok } },
+ },
+ '/admin/auth/forgot-password': {
+ post: { tags: ['Admin'], summary: 'Admin forgot password', security: [], responses: { '200': ok } },
+ },
+ '/admin/auth/reset-password': {
+ post: { tags: ['Admin'], summary: 'Admin reset password', security: [], responses: { '200': ok } },
+ },
+ '/admin/auth/me': {
+ get: { tags: ['Admin'], summary: 'Current admin profile', responses: { '200': ok } },
+ },
+ '/admin/auth/2fa/setup': {
+ post: { tags: ['Admin'], summary: 'Setup 2FA', responses: { '200': ok } },
+ },
+ '/admin/auth/2fa/verify': {
+ post: { tags: ['Admin'], summary: 'Verify 2FA code', responses: { '200': ok } },
+ },
+ '/admin/companies': {
+ get: { tags: ['Admin'], summary: 'List all companies', responses: { '200': ok } },
+ },
+ '/admin/companies/{id}': {
+ get: { tags: ['Admin'], summary: 'Get company', parameters: [idPath()], responses: { '200': ok } },
+ patch: { tags: ['Admin'], summary: 'Update company', parameters: [idPath()], responses: { '200': ok } },
+ delete: { tags: ['Admin'], summary: 'Delete company (Admin)', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/admin/companies/{id}/status': {
+ patch: { tags: ['Admin'], summary: 'Update company status', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/admin/companies/{id}/impersonate': {
+ post: { tags: ['Admin'], summary: 'Impersonate company (Admin)', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/admin/renters': {
+ get: { tags: ['Admin'], summary: 'List renters', responses: { '200': ok } },
+ },
+ '/admin/renters/{id}/block': {
+ post: { tags: ['Admin'], summary: 'Block renter', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/admin/renters/{id}/unblock': {
+ post: { tags: ['Admin'], summary: 'Unblock renter', parameters: [idPath()], responses: { '200': ok } },
+ },
+ '/admin/metrics': {
+ get: { tags: ['Admin'], summary: 'Platform metrics (Finance)', responses: { '200': ok } },
+ },
+ '/admin/notifications': {
+ get: { tags: ['Admin'], summary: 'Admin notifications', responses: { '200': ok } },
+ },
+ '/admin/audit-logs': {
+ get: { tags: ['Admin'], summary: 'Audit logs (Admin)', responses: { '200': ok } },
+ },
+ '/admin/admins': {
+ get: { tags: ['Admin'], summary: 'List admins (Super Admin)', responses: { '200': ok } },
+ post: { tags: ['Admin'], summary: 'Create admin (Super Admin)', responses: { '201': ok } },
+ },
+ },
+}
diff --git a/apps/api/src/tests/integration/health.test.ts b/apps/api/src/tests/integration/health.test.ts
index 9642048..4bc8a42 100644
--- a/apps/api/src/tests/integration/health.test.ts
+++ b/apps/api/src/tests/integration/health.test.ts
@@ -17,5 +17,14 @@ describe('GET /health', () => {
expect(res.status).toBe(200)
expect(res.body.name).toBe('rentaldrivego-api')
expect(Array.isArray(res.body.routes)).toBe(true)
+ expect(res.body.swaggerUi).toBe('/docs')
+ expect(res.body.openApi).toBe('/api/v1/openapi.json')
+ })
+
+ it('GET /api/v1/openapi.json returns the OpenAPI document', async () => {
+ const res = await request(app).get('/api/v1/openapi.json')
+ expect(res.status).toBe(200)
+ expect(res.body.openapi).toBe('3.0.3')
+ expect(res.body.info?.title).toBe('RentalDriveGo API')
})
})
diff --git a/package-lock.json b/package-lock.json
index 76431b6..a718ee0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,8 +72,10 @@
"react-dom": "^18.3.1",
"resend": "^3.2.0",
"socket.io": "^4.7.5",
+ "swagger-ui-express": "^5.0.1",
"twilio": "^5.1.0",
- "zod": "^3.23.0"
+ "zod": "^3.23.0",
+ "zod-to-json-schema": "^3.25.2"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
@@ -89,6 +91,7 @@
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@types/supertest": "^6.0.2",
+ "@types/swagger-ui-express": "^4.1.8",
"supertest": "^7.0.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.0",
@@ -2692,6 +2695,13 @@
"win32"
]
},
+ "node_modules/@scarf/scarf": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
+ "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@@ -3167,6 +3177,17 @@
"@types/superagent": "^8.1.0"
}
},
+ "node_modules/@types/swagger-ui-express": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
+ "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*",
+ "@types/serve-static": "*"
+ }
+ },
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -9495,6 +9516,30 @@
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC"
},
+ "node_modules/swagger-ui-dist": {
+ "version": "5.32.6",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz",
+ "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@scarf/scarf": "=1.4.0"
+ }
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
+ "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
+ "license": "MIT",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
@@ -10688,6 +10733,15 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25.28 || ^4"
+ }
+ },
"packages/database": {
"name": "@rentaldrivego/database",
"version": "1.0.0",