fix signin signout and apply the new style
This commit is contained in:
+6
-1
@@ -15,7 +15,10 @@ import offersRouter from './modules/offers/offer.routes'
|
||||
import analyticsRouter from './modules/analytics/analytics.routes'
|
||||
import notificationsRouter from './modules/notifications/notification.routes'
|
||||
import adminRouter from './modules/admin/admin.routes'
|
||||
import subscriptionsRouter from './modules/subscriptions/subscription.routes'
|
||||
import subscriptionsRouter, {
|
||||
subscriptionPublicRouter,
|
||||
subscriptionWebhookRouter,
|
||||
} from './modules/subscriptions/subscription.routes'
|
||||
import paymentsRouter from './modules/payments/payment.routes'
|
||||
import customersRouter from './modules/customers/customer.routes'
|
||||
import vehiclesRouter from './modules/vehicles/vehicle.routes'
|
||||
@@ -113,6 +116,8 @@ export function createApp() {
|
||||
|
||||
app.use(`${v1}/marketplace`, publicLimiter, marketplaceRouter)
|
||||
app.use(`${v1}/site`, publicLimiter, siteRouter)
|
||||
app.use(`${v1}/subscriptions`, subscriptionPublicRouter)
|
||||
app.use(`${v1}/subscriptions`, subscriptionWebhookRouter)
|
||||
|
||||
app.use(`${v1}/vehicles`, apiLimiter, vehiclesRouter)
|
||||
app.use(`${v1}/reservations`, apiLimiter, reservationsRouter)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
{
|
||||
"step": "1",
|
||||
"title": "Create your company workspace",
|
||||
"body": "Pick a plan, launch your 14-day trial, and verify the owner account."
|
||||
"body": "Pick a plan, launch your 90-day trial, and verify the owner account."
|
||||
},
|
||||
{
|
||||
"step": "2",
|
||||
@@ -156,7 +156,7 @@
|
||||
{
|
||||
"step": "1",
|
||||
"title": "Créez votre espace entreprise",
|
||||
"body": "Choisissez un plan, lancez votre essai de 14 jours et vérifiez le compte propriétaire."
|
||||
"body": "Choisissez un plan, lancez votre essai de 90 jours et vérifiez le compte propriétaire."
|
||||
},
|
||||
{
|
||||
"step": "2",
|
||||
|
||||
@@ -85,7 +85,7 @@ cron.schedule('0 9 * * *', async () => {
|
||||
const owner = sub.company.employees[0]
|
||||
if (owner) {
|
||||
await prisma.notification.create({
|
||||
data: { type: 'SUBSCRIPTION_TRIAL_ENDING', title: 'Your trial ends soon', body: 'Your 14-day free trial ends in 3 days. Add a payment method to keep access.', companyId: sub.companyId, employeeId: owner.id, channel: 'IN_APP', status: 'DELIVERED' },
|
||||
data: { type: 'SUBSCRIPTION_TRIAL_ENDING', title: 'Your trial ends soon', body: 'Your 90-day free trial ends in 3 days. Add a payment method to keep access.', companyId: sub.companyId, employeeId: owner.id, channel: 'IN_APP', status: 'DELIVERED' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { companySignupSchema } from './auth.company.schemas'
|
||||
import type { output } from 'zod'
|
||||
|
||||
type CompanySignupInput = output<typeof companySignupSchema>
|
||||
const TRIAL_PERIOD_DAYS = 90
|
||||
|
||||
function slugify(value: string) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 50) || 'company'
|
||||
@@ -37,7 +38,7 @@ export async function signup(body: CompanySignupInput) {
|
||||
|
||||
const slug = await generateUniqueSlug(body.companyName)
|
||||
const now = new Date()
|
||||
const trialEndAt = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000)
|
||||
const trialEndAt = new Date(now.getTime() + TRIAL_PERIOD_DAYS * 24 * 60 * 60 * 1000)
|
||||
const passwordHash = await bcrypt.hash(body.password, 12)
|
||||
|
||||
const result = await prisma.$transaction((tx) => repo.createCompanySignup(tx, {
|
||||
|
||||
@@ -31,14 +31,12 @@ export async function findListedCompanies(where: any, skip: number, take: number
|
||||
})
|
||||
}
|
||||
|
||||
export async function findPublishedVehicles(where: any, skip: number, take: number) {
|
||||
export async function findPublishedVehicles(where: any) {
|
||||
return prisma.vehicle.findMany({
|
||||
where,
|
||||
include: {
|
||||
company: { include: { brand: { select: { displayName: true, logoUrl: true, subdomain: true, marketplaceRating: true, primaryColor: true } } } },
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
orderBy: { dailyRate: 'asc' },
|
||||
})
|
||||
}
|
||||
@@ -62,7 +60,8 @@ export async function upsertMarketplaceCustomer(companyId: string, data: {
|
||||
|
||||
export async function createMarketplaceReservation(data: {
|
||||
companyId: string; vehicleId: string; customerId: string
|
||||
startDate: Date; endDate: Date; dailyRate: number; totalDays: number; totalAmount: number; notes?: string
|
||||
startDate: Date; endDate: Date; pickupLocation?: string | null; returnLocation?: string | null
|
||||
dailyRate: number; totalDays: number; totalAmount: number; notes?: string
|
||||
}) {
|
||||
return prisma.reservation.create({
|
||||
data: { ...data, source: 'MARKETPLACE', status: 'DRAFT' },
|
||||
|
||||
@@ -7,6 +7,9 @@ export const paginationSchema = z.object({
|
||||
|
||||
export const searchSchema = z.object({
|
||||
city: z.string().trim().max(100).optional(),
|
||||
pickupLocation: z.string().trim().max(100).optional(),
|
||||
dropoffLocation: z.string().trim().max(100).optional(),
|
||||
dropoffMode: z.enum(['same', 'different']).optional(),
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
category: z.string().trim().max(50).optional(),
|
||||
@@ -30,6 +33,8 @@ export const marketplaceReservationSchema = z.object({
|
||||
phone: z.string().max(30).optional(),
|
||||
startDate: z.string().datetime(),
|
||||
endDate: z.string().datetime(),
|
||||
pickupLocation: z.string().trim().max(100).optional(),
|
||||
returnLocation: z.string().trim().max(100).optional(),
|
||||
notes: z.string().max(500).optional(),
|
||||
language: z.enum(['en', 'fr', 'ar']).default('fr'),
|
||||
})
|
||||
|
||||
@@ -4,6 +4,46 @@ import { sendNotification, sendTransactionalEmail } from '../../services/notific
|
||||
import { marketplaceReservationEmail, type Lang } from '../../lib/emailTranslations'
|
||||
import * as repo from './marketplace.repo'
|
||||
|
||||
function normalizeLocation(value?: string | null) {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? normalized : null
|
||||
}
|
||||
|
||||
function normalizeLocationList(values?: string[] | null) {
|
||||
if (!Array.isArray(values)) return []
|
||||
return values
|
||||
.map((value) => normalizeLocation(value))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
}
|
||||
|
||||
function includesLocation(values: string[], target: string) {
|
||||
const key = target.toLocaleLowerCase()
|
||||
return values.some((value) => value.toLocaleLowerCase() === key)
|
||||
}
|
||||
|
||||
function matchesVehicleLocationRules(
|
||||
vehicle: { pickupLocations?: string[]; dropoffLocations?: string[]; allowDifferentDropoff?: boolean },
|
||||
params: { pickupLocation?: string; dropoffLocation?: string; dropoffMode?: 'same' | 'different' },
|
||||
) {
|
||||
const pickupLocation = normalizeLocation(params.pickupLocation)
|
||||
const dropoffLocation = normalizeLocation(params.dropoffLocation)
|
||||
const pickupLocations = normalizeLocationList(vehicle.pickupLocations)
|
||||
const dropoffLocations = normalizeLocationList(vehicle.dropoffLocations)
|
||||
|
||||
if (pickupLocation && pickupLocations.length > 0 && !includesLocation(pickupLocations, pickupLocation)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.dropoffMode === 'different') {
|
||||
if (!vehicle.allowDifferentDropoff) return false
|
||||
if (dropoffLocation && dropoffLocations.length > 0 && !includesLocation(dropoffLocations, dropoffLocation)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function getPublicOffers() {
|
||||
return repo.findPublicOffers()
|
||||
}
|
||||
@@ -34,7 +74,8 @@ export async function getListedCompanies(params: {
|
||||
}
|
||||
|
||||
export async function searchVehicles(params: {
|
||||
city?: string; startDate?: string; endDate?: string; category?: string
|
||||
city?: string; pickupLocation?: string; dropoffLocation?: string; dropoffMode?: 'same' | 'different'
|
||||
startDate?: string; endDate?: string; category?: string
|
||||
maxPrice?: number; transmission?: string; make?: string; model?: string
|
||||
page?: number; pageSize?: number
|
||||
}) {
|
||||
@@ -54,10 +95,12 @@ export async function searchVehicles(params: {
|
||||
where.company = { ...where.company, brand: { isListedOnMarketplace: true, publicCity: { contains: params.city, mode: 'insensitive' } } }
|
||||
}
|
||||
|
||||
const vehicles = await repo.findPublishedVehicles(where, (page - 1) * pageSize, pageSize)
|
||||
const vehicles = await repo.findPublishedVehicles(where)
|
||||
const locationFiltered = vehicles.filter((vehicle) => matchesVehicleLocationRules(vehicle, params))
|
||||
const pagedVehicles = locationFiltered.slice((page - 1) * pageSize, page * pageSize)
|
||||
|
||||
const availability = await Promise.all(
|
||||
vehicles.map(async (v) => {
|
||||
pagedVehicles.map(async (v) => {
|
||||
const a = await getVehicleAvailabilitySummary(v.id, {
|
||||
range: params.startDate && params.endDate
|
||||
? { startDate: new Date(params.startDate), endDate: new Date(params.endDate) }
|
||||
@@ -68,7 +111,7 @@ export async function searchVehicles(params: {
|
||||
)
|
||||
const availMap = new Map(availability)
|
||||
|
||||
return vehicles.map((v) => ({
|
||||
return pagedVehicles.map((v) => ({
|
||||
...v,
|
||||
availability: availMap.get(v.id)?.available ?? null,
|
||||
availabilityStatus: availMap.get(v.id)?.status ?? 'AVAILABLE',
|
||||
@@ -79,7 +122,7 @@ 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
|
||||
notes?: string; language?: string
|
||||
pickupLocation?: string; returnLocation?: string; notes?: string; language?: string
|
||||
}) {
|
||||
const startDate = new Date(body.startDate)
|
||||
const endDate = new Date(body.endDate)
|
||||
@@ -91,6 +134,24 @@ export async function createMarketplaceReservation(body: {
|
||||
const vehicle = await repo.findVehicleForMarketplace(body.vehicleId, body.companySlug)
|
||||
if (!vehicle) throw new NotFoundError('Vehicle not found')
|
||||
|
||||
const pickupLocation = normalizeLocation(body.pickupLocation)
|
||||
const returnLocation = normalizeLocation(body.returnLocation) ?? pickupLocation
|
||||
const pickupLocations = normalizeLocationList(vehicle.pickupLocations)
|
||||
const dropoffLocations = normalizeLocationList(vehicle.dropoffLocations)
|
||||
|
||||
if (pickupLocation && pickupLocations.length > 0 && !includesLocation(pickupLocations, pickupLocation)) {
|
||||
throw new AppError('Selected pickup location is not available for this vehicle', 400, 'invalid_pickup_location')
|
||||
}
|
||||
|
||||
if (pickupLocation && returnLocation && pickupLocation.toLocaleLowerCase() !== returnLocation.toLocaleLowerCase()) {
|
||||
if (!vehicle.allowDifferentDropoff) {
|
||||
throw new AppError('This vehicle must be returned to the same pickup location', 400, 'same_dropoff_required')
|
||||
}
|
||||
if (dropoffLocations.length > 0 && !includesLocation(dropoffLocations, returnLocation)) {
|
||||
throw new AppError('Selected return location is not available for this vehicle', 400, 'invalid_return_location')
|
||||
}
|
||||
}
|
||||
|
||||
const availability = await getVehicleAvailabilitySummary(body.vehicleId, { range: { startDate, endDate } })
|
||||
if (!availability.available) {
|
||||
throw new AppError('Vehicle is not available for the selected dates', 409, 'unavailable', {
|
||||
@@ -107,7 +168,7 @@ export async function createMarketplaceReservation(body: {
|
||||
|
||||
const reservation = await repo.createMarketplaceReservation({
|
||||
companyId: vehicle.companyId, vehicleId: body.vehicleId, customerId: customer.id,
|
||||
startDate, endDate, dailyRate: vehicle.dailyRate, totalDays, totalAmount, notes: body.notes,
|
||||
startDate, endDate, pickupLocation, returnLocation, dailyRate: vehicle.dailyRate, totalDays, totalAmount, notes: body.notes,
|
||||
})
|
||||
|
||||
const lang = (body.language ?? 'fr') as Lang
|
||||
|
||||
@@ -52,6 +52,9 @@ function makeVehicle(overrides: object = {}) {
|
||||
return {
|
||||
id: 'v-1', dailyRate: 10000, year: 2022, make: 'Toyota', model: 'Camry',
|
||||
isPublished: true, status: 'AVAILABLE', companyId: 'co-1',
|
||||
pickupLocations: ['Casablanca'],
|
||||
allowDifferentDropoff: false,
|
||||
dropoffLocations: [],
|
||||
company: { name: 'Test Co', brand: { displayName: 'Test Co' } },
|
||||
...overrides,
|
||||
}
|
||||
@@ -98,6 +101,19 @@ describe('searchVehicles', () => {
|
||||
})
|
||||
expect(result[0].availability).toBe(false)
|
||||
})
|
||||
|
||||
it('filters out vehicles that do not support different drop-off when requested', async () => {
|
||||
vi.mocked(repo.findPublishedVehicles).mockResolvedValue([
|
||||
makeVehicle({ id: 'same-only' }),
|
||||
makeVehicle({ id: 'one-way', allowDifferentDropoff: true, dropoffLocations: ['Rabat'] }),
|
||||
] as any)
|
||||
vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null })
|
||||
|
||||
const result = await searchVehicles({ pickupLocation: 'Casablanca', dropoffLocation: 'Rabat', dropoffMode: 'different' })
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe('one-way')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -114,8 +130,12 @@ describe('createMarketplaceReservation', () => {
|
||||
vi.mocked(repo.upsertMarketplaceCustomer).mockResolvedValue({ id: 'c-1' } as any)
|
||||
vi.mocked(repo.createMarketplaceReservation).mockResolvedValue({ id: 'r-1' } as any)
|
||||
|
||||
const result = await createMarketplaceReservation(baseBody)
|
||||
const result = await createMarketplaceReservation({ ...baseBody, pickupLocation: 'Casablanca', returnLocation: 'Casablanca' })
|
||||
expect(result.reservationId).toBe('r-1')
|
||||
expect(repo.createMarketplaceReservation).toHaveBeenCalledWith(expect.objectContaining({
|
||||
pickupLocation: 'Casablanca',
|
||||
returnLocation: 'Casablanca',
|
||||
}))
|
||||
})
|
||||
|
||||
it('throws NotFoundError when vehicle not found', async () => {
|
||||
@@ -131,6 +151,14 @@ describe('createMarketplaceReservation', () => {
|
||||
await expect(createMarketplaceReservation(baseBody)).rejects.toMatchObject({ error: 'unavailable' })
|
||||
})
|
||||
|
||||
it('rejects different return locations when the vehicle requires same drop-off', async () => {
|
||||
vi.mocked(repo.findVehicleForMarketplace).mockResolvedValue(makeVehicle() as any)
|
||||
|
||||
await expect(
|
||||
createMarketplaceReservation({ ...baseBody, pickupLocation: 'Casablanca', returnLocation: 'Rabat' }),
|
||||
).rejects.toMatchObject({ error: 'same_dropoff_required' })
|
||||
})
|
||||
|
||||
it('throws AppError for invalid date range', async () => {
|
||||
await expect(
|
||||
createMarketplaceReservation({ ...baseBody, startDate: '2025-06-04T00:00:00.000Z', endDate: '2025-06-01T00:00:00.000Z' }),
|
||||
|
||||
@@ -9,21 +9,23 @@ import * as paypal from '../../services/paypalService'
|
||||
import * as service from './subscription.service'
|
||||
import { checkoutSchema, changePlanSchema, capturePaypalSchema } from './subscription.schemas'
|
||||
|
||||
const publicRouter = Router()
|
||||
const webhookRouter = Router()
|
||||
const router = Router()
|
||||
|
||||
// ─── Public ────────────────────────────────────────────────────
|
||||
|
||||
router.get('/plans', (_req, res) => {
|
||||
publicRouter.get('/plans', (_req, res) => {
|
||||
ok(res, service.getPlans())
|
||||
})
|
||||
|
||||
router.get('/providers', (_req, res) => {
|
||||
publicRouter.get('/providers', (_req, res) => {
|
||||
ok(res, service.getProviders())
|
||||
})
|
||||
|
||||
// ─── Webhooks (no auth) ────────────────────────────────────────
|
||||
|
||||
router.post('/webhooks/amanpay', async (req, res, next) => {
|
||||
webhookRouter.post('/webhooks/amanpay', async (req, res, next) => {
|
||||
try {
|
||||
const rawBody = JSON.stringify(req.body)
|
||||
const signature = (req.headers['x-amanpay-signature'] as string) ?? ''
|
||||
@@ -35,7 +37,7 @@ router.post('/webhooks/amanpay', async (req, res, next) => {
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/webhooks/paypal', async (req, res, next) => {
|
||||
webhookRouter.post('/webhooks/paypal', async (req, res, next) => {
|
||||
try {
|
||||
const rawBody = JSON.stringify(req.body)
|
||||
const isValid = await paypal.verifyWebhookEvent(req.headers as Record<string, string>, rawBody)
|
||||
@@ -97,3 +99,4 @@ router.post('/resume', requireRole('OWNER'), async (req, res, next) => {
|
||||
})
|
||||
|
||||
export default router
|
||||
export { publicRouter as subscriptionPublicRouter, webhookRouter as subscriptionWebhookRouter }
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
function normalizeLocations(value: unknown) {
|
||||
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []
|
||||
}
|
||||
|
||||
export function presentVehicle(vehicle: any) {
|
||||
return vehicle
|
||||
return {
|
||||
...vehicle,
|
||||
pickupLocations: normalizeLocations(vehicle?.pickupLocations),
|
||||
allowDifferentDropoff: Boolean(vehicle?.allowDifferentDropoff),
|
||||
dropoffLocations: normalizeLocations(vehicle?.dropoffLocations),
|
||||
}
|
||||
}
|
||||
|
||||
export function presentVehicleList(vehicles: any[], meta: { total: number; page: number; pageSize: number; totalPages: number }) {
|
||||
|
||||
@@ -17,6 +17,9 @@ export const vehicleSchema = z.object({
|
||||
notes: z.string().optional(),
|
||||
isPublished: z.boolean().default(true),
|
||||
status: z.enum(['AVAILABLE', 'RENTED', 'MAINTENANCE', 'OUT_OF_SERVICE']).optional(),
|
||||
pickupLocations: z.array(z.string().trim().min(1)).default([]),
|
||||
allowDifferentDropoff: z.boolean().default(false),
|
||||
dropoffLocations: z.array(z.string().trim().min(1)).default([]),
|
||||
})
|
||||
|
||||
export const listQuerySchema = z.object({
|
||||
|
||||
@@ -3,6 +3,42 @@ import { NotFoundError, ValidationError } from '../../http/errors'
|
||||
import { presentVehicle, presentVehicleList } from './vehicle.presenter'
|
||||
import * as repo from './vehicle.repo'
|
||||
|
||||
function normalizeLocations(locations: unknown) {
|
||||
if (!Array.isArray(locations)) return []
|
||||
const seen = new Set<string>()
|
||||
const normalized: string[] = []
|
||||
for (const entry of locations) {
|
||||
if (typeof entry !== 'string') continue
|
||||
const value = entry.trim()
|
||||
const key = value.toLocaleLowerCase()
|
||||
if (!value || seen.has(key)) continue
|
||||
seen.add(key)
|
||||
normalized.push(value)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function applyLocationSettings(data: any) {
|
||||
const patch = { ...data }
|
||||
|
||||
if (patch.pickupLocations !== undefined) {
|
||||
patch.pickupLocations = normalizeLocations(patch.pickupLocations)
|
||||
}
|
||||
if (patch.dropoffLocations !== undefined) {
|
||||
patch.dropoffLocations = normalizeLocations(patch.dropoffLocations)
|
||||
}
|
||||
|
||||
if (patch.allowDifferentDropoff === false) {
|
||||
patch.dropoffLocations = []
|
||||
}
|
||||
|
||||
if (patch.allowDifferentDropoff === true && patch.dropoffLocations !== undefined && patch.dropoffLocations.length === 0) {
|
||||
throw new ValidationError('Drop-off locations are required when different drop-off is enabled')
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
||||
|
||||
export async function listVehicles(companyId: string, query: { status?: string; category?: string; published?: string; page?: number; pageSize?: number }) {
|
||||
const page = query.page ?? 1
|
||||
const pageSize = query.pageSize ?? 20
|
||||
@@ -22,11 +58,11 @@ export async function getVehicle(id: string, companyId: string) {
|
||||
}
|
||||
|
||||
export async function createVehicle(data: any, companyId: string) {
|
||||
return presentVehicle(await repo.create({ ...data, companyId }))
|
||||
return presentVehicle(await repo.create({ ...applyLocationSettings(data), companyId }))
|
||||
}
|
||||
|
||||
export async function updateVehicle(id: string, companyId: string, data: any) {
|
||||
const patch = { ...data }
|
||||
const patch = applyLocationSettings(data)
|
||||
if (patch.status === 'MAINTENANCE' || patch.status === 'OUT_OF_SERVICE') {
|
||||
patch.isPublished = false
|
||||
} else if (patch.status === 'AVAILABLE' || patch.status === 'RENTED') {
|
||||
|
||||
@@ -52,6 +52,9 @@ export async function createVehicle(companyId: string, overrides: Record<string,
|
||||
dailyRate: 500,
|
||||
status: 'AVAILABLE',
|
||||
isPublished: true,
|
||||
pickupLocations: ['Casablanca'],
|
||||
allowDifferentDropoff: false,
|
||||
dropoffLocations: [],
|
||||
...overrides,
|
||||
} as any,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import request from 'supertest'
|
||||
import { createApp } from '../../app'
|
||||
|
||||
const app = createApp()
|
||||
|
||||
function uniqueEmail(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@test.com`
|
||||
}
|
||||
|
||||
describe('Company signup API', () => {
|
||||
it('creates new accounts with a 90-day trial period', async () => {
|
||||
const startedAt = Date.now()
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/v1/auth/company/signup')
|
||||
.send({
|
||||
firstName: 'Trial',
|
||||
lastName: 'Owner',
|
||||
email: uniqueEmail('trial-owner'),
|
||||
password: 'Password123!',
|
||||
companyName: `Trial Cars ${Date.now()}`,
|
||||
legalForm: 'LLC',
|
||||
registrationNumber: `REG-${Date.now()}`,
|
||||
streetAddress: '123 Main Street',
|
||||
city: 'Casablanca',
|
||||
country: 'Morocco',
|
||||
zipCode: '20000',
|
||||
companyPhone: '+212600000000',
|
||||
yearsActive: '1',
|
||||
preferredLanguage: 'en',
|
||||
plan: 'STARTER',
|
||||
billingPeriod: 'MONTHLY',
|
||||
currency: 'MAD',
|
||||
paymentProvider: 'AMANPAY',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
|
||||
const trialEndsAt = new Date(res.body.data.trialEndsAt).getTime()
|
||||
const trialDurationDays = (trialEndsAt - startedAt) / (24 * 60 * 60 * 1000)
|
||||
|
||||
expect(trialDurationDays).toBeGreaterThan(89.9)
|
||||
expect(trialDurationDays).toBeLessThan(90.1)
|
||||
})
|
||||
})
|
||||
@@ -64,11 +64,17 @@ describe('Vehicles API', () => {
|
||||
licensePlate: `TEST-${Date.now()}`,
|
||||
category: 'COMPACT',
|
||||
dailyRate: 400,
|
||||
pickupLocations: ['Casablanca Airport', 'Casablanca Downtown'],
|
||||
allowDifferentDropoff: true,
|
||||
dropoffLocations: ['Rabat Downtown'],
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.data.make).toBe('Honda')
|
||||
expect(res.body.data.model).toBe('Civic')
|
||||
expect(res.body.data.pickupLocations).toEqual(['Casablanca Airport', 'Casablanca Downtown'])
|
||||
expect(res.body.data.allowDifferentDropoff).toBe(true)
|
||||
expect(res.body.data.dropoffLocations).toEqual(['Rabat Downtown'])
|
||||
})
|
||||
|
||||
it('returns 400 for missing required fields', async () => {
|
||||
@@ -116,6 +122,24 @@ describe('Vehicles API', () => {
|
||||
expect(res.body.data.notes).toBe('Updated via integration test')
|
||||
})
|
||||
|
||||
it('updates pickup and drop-off settings', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/v1/vehicles/${vehicle.id}`)
|
||||
.set(authHeader(token))
|
||||
.send({
|
||||
pickupLocations: ['Casablanca Airport'],
|
||||
allowDifferentDropoff: true,
|
||||
dropoffLocations: ['Rabat Downtown', 'Marrakech Center'],
|
||||
})
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.body.data.pickupLocations).toEqual(['Casablanca Airport'])
|
||||
expect(res.body.data.allowDifferentDropoff).toBe(true)
|
||||
expect(res.body.data.dropoffLocations).toEqual(['Rabat Downtown', 'Marrakech Center'])
|
||||
})
|
||||
|
||||
it('sets isPublished=false when status=MAINTENANCE', async () => {
|
||||
const vehicle = await createVehicle(companyId)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user