Vendor Employee Detail — Wages & Attendance Payloads
Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.
Purpose
Section titled “Purpose”PUT /api/v1/vendor/vendor-employees/:id/wages and PUT /api/v1/vendor/vendor-employees/:id/attendance are the two write paths exposed by the vendor module’s employee-detail surface. Both accept numeric-string fields that the database stores in UNSIGNED decimal(...) columns. Without a server-side guard, a negative value (e.g. -20 typed into the wage-rate input) slips past IsNumberString (which only checks format) and MySQL rejects it at INSERT time with errno 1264 — “Out of range value for column ‘wage_rate’ at row 1” — producing a 500 from the controller.
This page documents the shared IsNonNegativeNumberString validator that fixes the issue at the DTO layer and the service-layer defensive guard.
Affected fields
Section titled “Affected fields”Every field below is paired with @IsOptional() + @IsNumberString() + the new @IsNonNegativeNumberString():
| DTO field | DB column | DB type | Source |
|---|---|---|---|
UpsertVendorEmployeeWageDto.wage_rate | vendor_employee_wages.wage_rate | decimal(12,2) UNSIGNED NULL | migration 2000000000165-CreateVendorEmployeeDetailAuxTables |
UpsertVendorEmployeeAttendanceMetricDto.on_time_rate | vendor_employee_attendance_metrics.on_time_rate | decimal(5,2) UNSIGNED NULL | same migration |
UpsertVendorEmployeeAttendanceMetricDto.avg_hours_per_week | …avg_hours_per_week | decimal(6,2) UNSIGNED NULL | same migration |
UpsertVendorEmployeeAttendanceMetricDto.average_shift_rating | …average_shift_rating | decimal(3,2) UNSIGNED NULL | same migration |
Contract
Section titled “Contract”- Empty /
undefinedvalues are treated as not provided — they bypass validation and the column is stored asNULL(where nullable). Pair with@IsOptional(). - Numeric strings
>= 0are accepted."0","0.00","12","12.50","9999999999.99"all pass. - Negative strings (
"-20","-0.01", …) are rejected with HTTP 400 carrying the validator’smessagefield. The NestJS ValidationPipe returns this as{ message: [...], error: "Bad Request", statusCode: 400 }. - Non-numeric strings (
"abc","-") are rejected by the underlying@IsNumberString()decorator before the new one runs.
Defence-in-depth: VendorEmployeeDetailService.replaceWages
Section titled “Defence-in-depth: VendorEmployeeDetailService.replaceWages”Even with the DTO validator wired in, the service re-checks every wage_rate before it calls repo.save(). If a payload still contains a negative value (for example because the global ValidationPipe is misconfigured, or a custom client bypassed it), the service throws:
{ "success": false, "errors": [ { "code": "invalid_wage_rate", "message": "wage_rate must be a non-negative number (>= 0)", "field": "wage_rate", "index": 1, "value": "-20" } ]}HTTP 400 Bad Request. No INSERT is issued for any row in the batch (the existing rows were already deleted by repo.delete({ employeeId }); on error we surface the failure rather than silently re-save the partial list — the client should retry with a corrected payload).
Frontend contract (vendor-web)
Section titled “Frontend contract (vendor-web)”The “Access, roles & wages” edit form (EmployeeAccessRolesWagesEditForm) wires the same rule at the Zod layer so the user sees a field-level error before submitting:
rate: z .union([z.string(), z.number()]) .transform((val) => (val === "" || val == null ? undefined : String(val))) .pipe( z.string().optional().refine( (val) => val === undefined || val === "" || (!Number.isNaN(Number(val)) && Number(val) >= 0), { message: t("employees.detail.accessRolesWages.validation.wageRateNonNegative", { defaultValue: "Wage rate must be 0 or greater" }) }, ), );The <input type="number" min="0" step="0.01" /> widget also blocks browser spinner negatives, but the Zod check is the source of truth (it covers paste, autofill, and screen-reader input).
- Validator unit tests:
apps/backend/src/modules/vendor/dto/validators/is-non-negative-number-string.validator.spec.ts(6 tests). - Service guard tests:
apps/backend/src/modules/vendor/vendor-employee-detail.service.spec.ts→describe('replaceWages — wage_rate validation')(4 tests: rejects-20, includesfield/index/valuein the 400 body, accepts0/positive, treatsundefined/''as not-provided). - Frontend schema tests:
apps/vendor-web/src/app/dashboard/employees/v2/components/lib/access-roles-wages-schema.spec.ts(6 tests).
Why reject instead of allowing negatives?
Section titled “Why reject instead of allowing negatives?”A wage rate is, by definition, a non-negative quantity of currency per unit time. Allowing -20 into vendor_employee_wages.wage_rate would corrupt downstream payroll calculations (totals, averages, tax deductions). The clean fix is to reject at the boundary, not to migrate the column to signed decimal — that would let bad data through and force every payroll consumer to defend against rate < 0.
If a future feature ever needs signed values (e.g. a “deduction” column), add a new decimal(...) column without UNSIGNED rather than relaxing this contract.