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
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:
@@ -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')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user