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
+52
View File
@@ -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.**
@@ -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')
)
}
+56
View File
@@ -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, 150 chars, Unicode letters/spaces/hyphens/apostrophes allowed |
| lastName | string | required, trim, 150 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, 530 chars, alphanumeric only |
| date (generic) | date | must be a valid ISO 8601 date (`YYYY-MM-DD`); reject free text |
| address | string | required, trim, 1255 chars |
| idPassport | string | required, trim, 530 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, 185 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.