fix mode
Build & Push / Pipeline Tests (push) Successful in 1m59s
Test / Type Check (all packages) (push) Successful in 52s
Build & Push / Build & Push Docker Image (push) Successful in 3m40s
Test / API Unit Tests (push) Successful in 1m12s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Carplace Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 43s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Successful in 1m8s

This commit is contained in:
root
2026-08-31 21:50:26 -04:00
parent cb8bf63218
commit b99e8d0be4
12 changed files with 199 additions and 12 deletions
@@ -30,6 +30,12 @@ describe('customer schema contracts', () => {
q: 'aya',
flagged: 'true',
})
expect(listQuerySchema.parse({ pageSize: '100', search: 'aya' })).toEqual({
page: 1,
pageSize: 100,
search: 'aya',
})
})
it('rejects impossible pagination and unsafe customer field lengths', () => {
@@ -25,6 +25,7 @@ export const customerSchema = z.object({
export const listQuerySchema = paginationSchema.extend({
q: z.string().max(100).optional(),
search: z.string().max(100).optional(),
flagged: z.enum(['true', 'false']).optional(),
})
@@ -54,6 +54,25 @@ describe('customer.service boundary behavior', () => {
expect(result.meta).toEqual({ total: 41, page: 3, pageSize: 10, totalPages: 5 })
})
it('accepts dashboard search query alias for customer lists', async () => {
vi.mocked(repo.findMany).mockResolvedValue([[], 0] as never)
await service.listCustomers('company_1', {
page: 1,
pageSize: 100,
search: ' aya ',
})
expect(repo.findMany).toHaveBeenCalledWith({
companyId: 'company_1',
OR: [
{ firstName: { contains: 'aya', mode: 'insensitive' } },
{ lastName: { contains: 'aya', mode: 'insensitive' } },
{ email: { contains: 'aya', mode: 'insensitive' } },
],
}, 0, 100)
})
it('normalizes customer date fields and swallows async license validation failures after create', async () => {
vi.mocked(repo.create).mockResolvedValue({ id: 'customer_1', email: 'renter@example.test' } as never)
vi.mocked(validateAndFlagLicense).mockRejectedValue(new Error('provider down') as never)
@@ -4,11 +4,11 @@ import { NotFoundError } from '../../http/errors'
import { presentCustomer, presentCustomerList } from './customer.presenter'
import * as repo from './customer.repo'
export async function listCustomers(companyId: string, query: { page?: number; pageSize?: number; q?: string; flagged?: string }) {
export async function listCustomers(companyId: string, query: { page?: number; pageSize?: number; q?: string; search?: string; flagged?: string }) {
const page = query.page ?? 1
const pageSize = query.pageSize ?? 20
const { q, flagged } = query
const safeQ = q ? q.trim().slice(0, 100) : undefined
const { q, search, flagged } = query
const safeQ = (q ?? search)?.trim().slice(0, 100) || undefined
const where: any = { companyId }
if (flagged !== undefined) where.flagged = flagged === 'true'
if (safeQ) {
@@ -40,6 +40,16 @@ describe('reservation.presenter boundary behavior', () => {
})
})
it('treats omitted contract and invoice numbers as not generated', () => {
expect(buildReservationWorkflow({
status: 'DRAFT',
extras: {},
})).toMatchObject({
contractGenerated: false,
coreEditable: true,
})
})
it('marks closed reservations as immutable and exposes close metadata from extras only when strings', () => {
expect(buildReservationWorkflow({
status: 'COMPLETED',
@@ -34,8 +34,8 @@ function readAddressField(address: unknown, key: string): string | null {
export function buildReservationWorkflow(reservation: {
status: string
contractNumber: string | null
invoiceNumber: string | null
contractNumber?: string | null
invoiceNumber?: string | null
extras: unknown
}) {
const extras = parseReservationExtras(reservation.extras)
@@ -147,8 +147,8 @@ export function serializeReservationForDashboard<T extends {
extras: unknown
status: string
source: string
contractNumber: string | null
invoiceNumber: string | null
contractNumber?: string | null
invoiceNumber?: string | null
paymentStatus?: string | null
customer?: DashboardReservationCustomer | null
}>(reservation: T): SerializedDashboardReservation<T> {
@@ -43,7 +43,21 @@ describe('reservation.repo edge queries', () => {
expect(prisma.reservation.findMany).toHaveBeenCalledWith({
where: { companyId: 'company_1' },
include: { vehicle: true, customer: true },
include: {
vehicle: true,
customer: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
driverLicense: true,
dateOfBirth: true,
address: true,
licenseValidationStatus: true,
},
},
},
skip: 20,
take: 10,
orderBy: { createdAt: 'desc' },
@@ -9,11 +9,22 @@ const FULL_INCLUDE = {
damageReports: true,
}
const DASHBOARD_LIST_CUSTOMER_SELECT = {
id: true,
firstName: true,
lastName: true,
email: true,
driverLicense: true,
dateOfBirth: true,
address: true,
licenseValidationStatus: true,
}
export async function findMany(where: any, skip: number, take: number) {
return Promise.all([
prisma.reservation.findMany({
where,
include: { vehicle: true, customer: true },
include: { vehicle: true, customer: { select: DASHBOARD_LIST_CUSTOMER_SELECT } },
skip,
take,
orderBy: { createdAt: 'desc' },
@@ -78,6 +78,19 @@ describe('vehicle pricing service boundaries', () => {
expect(repo.createPricingConfiguration).not.toHaveBeenCalled()
})
it('returns transient pricing when pricing migration columns or enums are missing', async () => {
vi.mocked(repo.findById).mockResolvedValue(vehicle as any)
vi.mocked(repo.findPricingConfiguration).mockRejectedValue({
code: 'P2022',
message: 'The column VehiclePricingMode does not exist',
})
const result = await service.getVehiclePricing('vehicle_1', 'company_1')
expect(result.configuration.id).toBe('transient-vehicle_1')
expect(repo.createPricingConfiguration).not.toHaveBeenCalled()
})
it('rejects pricing configuration bounds before persisting invalid rates', async () => {
vi.mocked(repo.findById).mockResolvedValue(vehicle as any)
vi.mocked(repo.findPricingConfiguration).mockResolvedValue(config as any)
@@ -17,11 +17,16 @@ const RULE_PRIORITY: Record<string, number> = {
function isPricingStorageMissing(error: unknown) {
if (!error || typeof error !== 'object') return false
const candidate = error as { code?: string; message?: string }
const message = candidate.message ?? ''
return (
candidate.code === 'P2021' ||
candidate.message?.includes('vehicle_pricing_configurations') === true ||
candidate.message?.includes('vehicle_pricing_rules') === true ||
candidate.message?.includes('vehicle_price_history') === true
candidate.code === 'P2022' ||
message.includes('vehicle_pricing_configurations') ||
message.includes('vehicle_pricing_rules') ||
message.includes('vehicle_price_history') ||
message.includes('VehiclePricingMode') ||
message.includes('VehiclePricingRuleType') ||
message.includes('VehiclePriceChangeSource')
)
}