From b99e8d0be4b74665821744e09864e1c0518f5951 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 31 Aug 2026 21:50:26 -0400 Subject: [PATCH] fix mode --- .codex/agent_instructions.md | 52 +++++++++++++++++ .../customer.schemas.contract.test.ts | 6 ++ .../src/modules/customers/customer.schemas.ts | 1 + .../customer.service.boundary.test.ts | 19 +++++++ .../src/modules/customers/customer.service.ts | 6 +- .../reservation.presenter.boundary.test.ts | 10 ++++ .../reservations/reservation.presenter.ts | 8 +-- .../reservation.repo.edge.test.ts | 16 +++++- .../modules/reservations/reservation.repo.ts | 13 ++++- .../vehicles/vehicle.pricing.service.test.ts | 13 +++++ .../src/modules/vehicles/vehicle.service.ts | 11 +++- input-validation-plan.md | 56 +++++++++++++++++++ 12 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 .codex/agent_instructions.md create mode 100644 input-validation-plan.md diff --git a/.codex/agent_instructions.md b/.codex/agent_instructions.md new file mode 100644 index 0000000..a77f898 --- /dev/null +++ b/.codex/agent_instructions.md @@ -0,0 +1,52 @@ +# Scope Discipline Rules + +These rules override any general instinct to "improve while I'm in there." Follow them on every task, no exceptions. + +## Before touching any code + +1. Restate the task in one sentence: what behavior must change, and what the expected outcome is. +2. Identify the smallest set of files/functions responsible for that behavior. This is your **allowed scope**. Everything else is **protected**. +3. Read the relevant code before editing it. Do not edit based on assumptions about how it probably works. + +## The hard rule: ask before expanding scope + +If, while working, you find that: +- a file outside your allowed scope needs to change, +- a dependency needs to be added/updated, +- a test needs modification, +- an unrelated bug is blocking you, +- or a "cleaner" implementation would touch more than the minimum, + +**stop and ask me before making that change.** Explain: +- what you were trying to do, +- why the fix requires going outside the original scope, +- exactly what you want to change and where. + +Wait for my answer. Do not proceed on your own judgment, even if you're confident it's correct or trivial. + +This applies even to small things (renaming a variable for clarity, fixing a typo in an unrelated comment, reformatting a block you had to scroll past). If it's not required to satisfy the request, ask first. + +## While editing + +- Make the fewest-line, fewest-file change that correctly satisfies the request. +- Preserve existing naming, structure, patterns, and formatting. Match the codebase's existing style, don't impose your own. +- Never run project-wide formatters/linters-with-autofix/import-organizers as a side effect of a small change. +- Never touch tests except to add new ones that validate the requested behavior — and only after confirming that's in scope. +- Treat any uncommitted/staged changes already in the working tree as off-limits. Don't revert, reset, or absorb them into your edit. + +## Before reporting done + +Review your own diff, file by file, line by line. For anything you can't justify with "this was required by the explicit request," revert it. + +Then report: +- **Files changed** — list, with a one-line reason each tied directly to the request. +- **Scope confirmation** — explicitly state: "No files, dependencies, tests, or config outside this list were modified." +- **Anything you noticed but didn't touch** — unrelated bugs, tech debt, cleanup opportunities. Mention them, don't fix them. + +## If the task genuinely can't be done without expanding scope + +Say so plainly, explain what would need to change and why, and wait for confirmation. Don't silently do the bigger version, and don't pretend a partial/incorrect fix is complete. + +--- + +**Default when uncertain: don't make the change, ask instead.** \ No newline at end of file diff --git a/apps/api/src/modules/customers/customer.schemas.contract.test.ts b/apps/api/src/modules/customers/customer.schemas.contract.test.ts index e90b245..e3c3a3a 100644 --- a/apps/api/src/modules/customers/customer.schemas.contract.test.ts +++ b/apps/api/src/modules/customers/customer.schemas.contract.test.ts @@ -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', () => { diff --git a/apps/api/src/modules/customers/customer.schemas.ts b/apps/api/src/modules/customers/customer.schemas.ts index 570a834..451e00e 100644 --- a/apps/api/src/modules/customers/customer.schemas.ts +++ b/apps/api/src/modules/customers/customer.schemas.ts @@ -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(), }) diff --git a/apps/api/src/modules/customers/customer.service.boundary.test.ts b/apps/api/src/modules/customers/customer.service.boundary.test.ts index db2f9fe..293f835 100644 --- a/apps/api/src/modules/customers/customer.service.boundary.test.ts +++ b/apps/api/src/modules/customers/customer.service.boundary.test.ts @@ -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) diff --git a/apps/api/src/modules/customers/customer.service.ts b/apps/api/src/modules/customers/customer.service.ts index 7237d9f..d09ce80 100644 --- a/apps/api/src/modules/customers/customer.service.ts +++ b/apps/api/src/modules/customers/customer.service.ts @@ -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) { diff --git a/apps/api/src/modules/reservations/reservation.presenter.boundary.test.ts b/apps/api/src/modules/reservations/reservation.presenter.boundary.test.ts index 8467120..31b840c 100644 --- a/apps/api/src/modules/reservations/reservation.presenter.boundary.test.ts +++ b/apps/api/src/modules/reservations/reservation.presenter.boundary.test.ts @@ -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', diff --git a/apps/api/src/modules/reservations/reservation.presenter.ts b/apps/api/src/modules/reservations/reservation.presenter.ts index d11cae2..36b94f4 100644 --- a/apps/api/src/modules/reservations/reservation.presenter.ts +++ b/apps/api/src/modules/reservations/reservation.presenter.ts @@ -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(reservation: T): SerializedDashboardReservation { diff --git a/apps/api/src/modules/reservations/reservation.repo.edge.test.ts b/apps/api/src/modules/reservations/reservation.repo.edge.test.ts index e5af25c..1946487 100644 --- a/apps/api/src/modules/reservations/reservation.repo.edge.test.ts +++ b/apps/api/src/modules/reservations/reservation.repo.edge.test.ts @@ -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' }, diff --git a/apps/api/src/modules/reservations/reservation.repo.ts b/apps/api/src/modules/reservations/reservation.repo.ts index 822485b..f5d341b 100644 --- a/apps/api/src/modules/reservations/reservation.repo.ts +++ b/apps/api/src/modules/reservations/reservation.repo.ts @@ -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' }, diff --git a/apps/api/src/modules/vehicles/vehicle.pricing.service.test.ts b/apps/api/src/modules/vehicles/vehicle.pricing.service.test.ts index 8a5fe10..98deb51 100644 --- a/apps/api/src/modules/vehicles/vehicle.pricing.service.test.ts +++ b/apps/api/src/modules/vehicles/vehicle.pricing.service.test.ts @@ -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) diff --git a/apps/api/src/modules/vehicles/vehicle.service.ts b/apps/api/src/modules/vehicles/vehicle.service.ts index 1e988f1..bff54c6 100644 --- a/apps/api/src/modules/vehicles/vehicle.service.ts +++ b/apps/api/src/modules/vehicles/vehicle.service.ts @@ -17,11 +17,16 @@ const RULE_PRIORITY: Record = { 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') ) } diff --git a/input-validation-plan.md b/input-validation-plan.md new file mode 100644 index 0000000..a1a9d8b --- /dev/null +++ b/input-validation-plan.md @@ -0,0 +1,56 @@ +# Task: Add Input Validation for User Data Form + +## Objective +Add length/format/range validation for the fields listed below, enforced at three layers: frontend (UX), backend (source of truth), and database (last line of defense). Do not change unrelated form behavior, styling, or fields not listed here. + +## Scope +Only touch the files responsible for these fields: the form component(s), the API endpoint(s) that receive this data, the validation schema/middleware, and the DB migration/schema for these columns. If a field doesn't exist yet in the DB/API, flag it and ask before creating new schema — don't assume the shape. + +--- + +## Field Rules + +| Field | Type | Rule | +|---|---|---| +| firstName | string | required, trim, 1–50 chars, Unicode letters/spaces/hyphens/apostrophes allowed | +| lastName | string | required, trim, 1–50 chars, same charset as firstName | +| email | string | required, trim, max 254 chars, valid email format (use an existing validation library, don't hand-roll regex) | +| nationality | enum | required, must be a valid ISO 3166-1 country code (dropdown-backed, not free text) | +| phoneNumber | string | required, normalize to E.164 on save (use existing phone validation lib if the project has one; otherwise ask before adding a dependency), max 20 raw input chars | +| driverLicense | string | optional unless told otherwise, trim, 5–30 chars, alphanumeric only | +| date (generic) | date | must be a valid ISO 8601 date (`YYYY-MM-DD`); reject free text | +| address | string | required, trim, 1–255 chars | +| idPassport | string | required, trim, 5–30 chars, alphanumeric | +| country | enum | required, valid ISO 3166-1 country code (dropdown-backed) | +| issuedDate | date | required, ISO 8601, must be ≤ today | +| expirationDate | date | required, ISO 8601, must be ≥ issuedDate | +| dateTime | datetime | required, store as ISO 8601 with UTC offset — do not store naive local time | +| vinNumber | string | required, exactly 17 chars, alphanumeric excluding I/O/Q, uppercase | +| city | string | required, trim, 1–85 chars | +| amount | number | required, numeric, > 0, max 2 decimal places, reasonable upper bound (ask what "reasonable" means for this use case if not obvious from context — don't guess a business rule) | + +## Cross-field rules +- `expirationDate` must be after `issuedDate`. +- `issuedDate` must not be in the future. +- Do not implement any cross-field rule not listed here without asking first. + +--- + +## Implementation steps + +1. **Locate existing validation approach.** Check if the project already uses a validation library (Zod, Yup, Joi, class-validator, pydantic, etc.) or a pattern for form validation. Use whatever already exists — do not introduce a new validation library if one is already in use. +2. **Frontend:** add `maxlength`/type-appropriate input constraints and inline error messages matching the rules above. Reuse existing form error-display patterns in the codebase. +3. **Backend:** add/extend the validation schema or middleware for the relevant endpoint(s) to enforce the same rules server-side. This is the authoritative check — assume the frontend check can be bypassed. +4. **Database:** only if column definitions currently allow unbounded/incorrect lengths, add matching length constraints via migration. Do not touch unrelated columns or tables. +5. Normalize country/nationality to ISO codes, phone to E.164, and dates to ISO 8601 at the point of validation/save — not scattered across multiple layers. + +## Explicitly out of scope (ask before doing any of this) +- Adding a new third-party validation/phone/country-list library if one doesn't already exist in the project. +- Changing the DB schema shape (splitting address into multiple columns, etc.) — flag it as a suggestion instead. +- Any field not in the list above. +- Deep format validation for driverLicense/idPassport beyond length + charset (these vary too much by issuing country to hardcode safely) — ask if stricter per-country validation is actually wanted. + +## Validation before reporting done +- Confirm each rule above has a corresponding check in frontend AND backend (list them field by field in your report). +- Confirm existing tests still pass; add new tests only for the fields touched here. +- Report any field where the existing DB column type/length conflicts with the rule above, rather than silently changing it.