mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
fix
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce one driver record per verified Fayda identity. A unique index on
|
||||||
|
* fayda_sub blocks a second driver from being created against the same Fayda
|
||||||
|
* OIDC subject; NULLs stay distinct so legacy/unverified rows are unaffected.
|
||||||
|
*/
|
||||||
|
export class AddDriverFaydaSubUnique1890000000006 implements MigrationInterface {
|
||||||
|
name = "AddDriverFaydaSubUnique1890000000006";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB"
|
||||||
|
ON freight.drivers (fayda_sub)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB"
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CreateDriverDto } from './dto/create-driver.dto';
|
import { CreateDriverDto } from './dto/create-driver.dto';
|
||||||
@@ -13,6 +13,12 @@ export class DriversService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(dto: CreateDriverDto): Promise<Driver> {
|
async create(dto: CreateDriverDto): Promise<Driver> {
|
||||||
|
if (dto.faydaVerified !== true) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Driver identity must be verified with Fayda before saving',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existing = await this.driverRepo.findOne({
|
const existing = await this.driverRepo.findOne({
|
||||||
where: [
|
where: [
|
||||||
{ licenseNumber: dto.licenseNumber },
|
{ licenseNumber: dto.licenseNumber },
|
||||||
@@ -33,6 +39,17 @@ export class DriversService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.faydaSub) {
|
||||||
|
const dupe = await this.driverRepo.findOne({
|
||||||
|
where: { faydaSub: dto.faydaSub },
|
||||||
|
});
|
||||||
|
if (dupe) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'A driver is already registered for this Fayda identity',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const driver = this.driverRepo.create(dto);
|
const driver = this.driverRepo.create(dto);
|
||||||
return this.driverRepo.save(driver);
|
return this.driverRepo.save(driver);
|
||||||
}
|
}
|
||||||
@@ -106,7 +123,25 @@ export class DriversService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.faydaSub && dto.faydaSub !== driver.faydaSub) {
|
||||||
|
const dupe = await this.driverRepo.findOne({
|
||||||
|
where: { faydaSub: dto.faydaSub },
|
||||||
|
});
|
||||||
|
if (dupe) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'A driver is already registered for this Fayda identity',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(driver, dto);
|
Object.assign(driver, dto);
|
||||||
|
|
||||||
|
if (driver.faydaVerified !== true) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Driver identity must be verified with Fayda before saving',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.driverRepo.save(driver);
|
return this.driverRepo.save(driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export class Driver extends BaseEntity {
|
|||||||
@Column({ name: 'fayda_verified', type: 'boolean', default: false, nullable: true })
|
@Column({ name: 'fayda_verified', type: 'boolean', default: false, nullable: true })
|
||||||
faydaVerified?: boolean;
|
faydaVerified?: boolean;
|
||||||
|
|
||||||
/** Fayda OIDC subject the identity was verified against. */
|
/** Fayda OIDC subject the identity was verified against. Unique — one driver
|
||||||
@Column({ name: 'fayda_sub', type: 'varchar', nullable: true })
|
* record per verified Fayda identity (NULLs allowed for legacy/unverified). */
|
||||||
|
@Column({ name: 'fayda_sub', type: 'varchar', unique: true, nullable: true })
|
||||||
faydaSub?: string | null;
|
faydaSub?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ const FleetFormDialog = ({
|
|||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
// Hard gate: a driver record cannot be saved until its identity is verified
|
||||||
|
// with Fayda. Mirrored server-side in DriversService.
|
||||||
|
if (verifyWithFayda && !faydaVerified) {
|
||||||
|
setFaydaError("Verify the driver's identity with Fayda before saving.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!validate()) return;
|
if (!validate()) return;
|
||||||
const payload = Object.fromEntries(
|
const payload = Object.fromEntries(
|
||||||
Object.entries(values)
|
Object.entries(values)
|
||||||
@@ -278,6 +284,9 @@ const FleetFormDialog = ({
|
|||||||
const renderField = (field: FleetFormFieldDef) => {
|
const renderField = (field: FleetFormFieldDef) => {
|
||||||
const value = values[field.name];
|
const value = values[field.name];
|
||||||
const error = errors[field.name];
|
const error = errors[field.name];
|
||||||
|
// Fayda-owned identity fields (name/email/phone/DOB/gender) are populated
|
||||||
|
// only by verification and never hand-edited.
|
||||||
|
const isDisabled = Boolean(field.disabled || field.faydaLocked);
|
||||||
|
|
||||||
if (field.type === "select") {
|
if (field.type === "select") {
|
||||||
return (
|
return (
|
||||||
@@ -297,7 +306,7 @@ const FleetFormDialog = ({
|
|||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
searchable
|
searchable
|
||||||
disabled={selectOptionsLoading}
|
disabled={selectOptionsLoading || isDisabled}
|
||||||
rightSection={
|
rightSection={
|
||||||
selectOptionsLoading ? (
|
selectOptionsLoading ? (
|
||||||
<Loader2 size={14} className="animate-spin" />
|
<Loader2 size={14} className="animate-spin" />
|
||||||
@@ -327,7 +336,7 @@ const FleetFormDialog = ({
|
|||||||
error={error}
|
error={error}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
disabled={selectOptionsLoading}
|
disabled={selectOptionsLoading || isDisabled}
|
||||||
rightSection={
|
rightSection={
|
||||||
selectOptionsLoading ? (
|
selectOptionsLoading ? (
|
||||||
<Loader2 size={14} className="animate-spin" />
|
<Loader2 size={14} className="animate-spin" />
|
||||||
@@ -351,7 +360,7 @@ const FleetFormDialog = ({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
disabled={field.disabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -371,7 +380,7 @@ const FleetFormDialog = ({
|
|||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
minRows={3}
|
minRows={3}
|
||||||
disabled={field.disabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -391,7 +400,7 @@ const FleetFormDialog = ({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
disabled={field.disabled}
|
disabled={isDisabled}
|
||||||
description={field.description || "Select a date"}
|
description={field.description || "Select a date"}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon size="sm" variant="subtle" color="green">
|
<ActionIcon size="sm" variant="subtle" color="green">
|
||||||
@@ -429,7 +438,7 @@ const FleetFormDialog = ({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
error={error}
|
error={error}
|
||||||
disabled={field.disabled}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -457,7 +466,8 @@ const FleetFormDialog = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Verify the driver's identity with Fayda to prefill their details.
|
Identity must be verified with Fayda before this driver can be
|
||||||
|
saved.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -489,6 +499,7 @@ const FleetFormDialog = ({
|
|||||||
color="edr-green"
|
color="edr-green"
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
disabled={verifyWithFayda && !faydaVerified}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ export const driversConfig: FleetResourceConfig = {
|
|||||||
],
|
],
|
||||||
formFields: [
|
formFields: [
|
||||||
{ name: "licenseNumber", label: "License Number", type: "text", required: true },
|
{ name: "licenseNumber", label: "License Number", type: "text", required: true },
|
||||||
{ name: "firstName", label: "First Name", type: "text", required: true },
|
{ name: "firstName", label: "First Name", type: "text", required: true, faydaLocked: true },
|
||||||
{ name: "lastName", label: "Last Name", type: "text", required: true },
|
{ name: "lastName", label: "Last Name", type: "text", required: true, faydaLocked: true },
|
||||||
{ name: "email", label: "Email", type: "email", required: true },
|
{ name: "email", label: "Email", type: "email", required: true, faydaLocked: true },
|
||||||
{ name: "phoneNumber", label: "Phone Number", type: "text", required: true },
|
{ name: "phoneNumber", label: "Phone Number", type: "text", required: true, faydaLocked: true },
|
||||||
{ name: "dateOfBirth", label: "Date of Birth", type: "date", required: true },
|
{ name: "dateOfBirth", label: "Date of Birth", type: "date", required: true, faydaLocked: true },
|
||||||
{ name: "gender", label: "Gender", type: "select", options: DRIVER_GENDER_OPTIONS },
|
{ name: "gender", label: "Gender", type: "select", options: DRIVER_GENDER_OPTIONS, faydaLocked: true },
|
||||||
{ name: "licenseExpiryDate", label: "License Expiry Date", type: "date", required: true },
|
{ name: "licenseExpiryDate", label: "License Expiry Date", type: "date", required: true },
|
||||||
{ name: "status", label: "Status", type: "select", required: true, options: DRIVER_STATUS_OPTIONS },
|
{ name: "status", label: "Status", type: "select", required: true, options: DRIVER_STATUS_OPTIONS },
|
||||||
{ name: "vehicleTypesAuthorized", label: "Authorized Vehicle Types", type: "multiselect", options: VEHICLE_TYPE_OPTIONS },
|
{ name: "vehicleTypesAuthorized", label: "Authorized Vehicle Types", type: "multiselect", options: VEHICLE_TYPE_OPTIONS },
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export interface FleetResourceColumn {
|
|||||||
export interface FleetFormFieldDef extends FormFieldDef {
|
export interface FleetFormFieldDef extends FormFieldDef {
|
||||||
dynamicOptions?: FleetDynamicOptions;
|
dynamicOptions?: FleetDynamicOptions;
|
||||||
noneOption?: boolean;
|
noneOption?: boolean;
|
||||||
|
/**
|
||||||
|
* Field is owned by the Fayda identity — populated only by verification and
|
||||||
|
* never hand-edited. Rendered disabled in the form.
|
||||||
|
*/
|
||||||
|
faydaLocked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FleetListFilterDef {
|
export interface FleetListFilterDef {
|
||||||
|
|||||||
Reference in New Issue
Block a user