forked from Yara724/api
YARA-899
This commit is contained in:
@@ -7,6 +7,13 @@ import {
|
|||||||
} from "src/client/dto/create-client.dto";
|
} from "src/client/dto/create-client.dto";
|
||||||
import { ClientDbService } from "./entities/db-service/client.db.service";
|
import { ClientDbService } from "./entities/db-service/client.db.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System-wide default applied when a client document has no
|
||||||
|
* `settings.carBodyAccidentMaxAgeDays` value yet. Keep it conservative so the
|
||||||
|
* gate is enforced even for older / unconfigured clients.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS = 7;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClientService {
|
export class ClientService {
|
||||||
constructor(private readonly clientDbService: ClientDbService) {}
|
constructor(private readonly clientDbService: ClientDbService) {}
|
||||||
@@ -53,4 +60,34 @@ export class ClientService {
|
|||||||
const list = client.map((element) => new ClientLists(element));
|
const list = client.map((element) => new ClientLists(element));
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the per-client CAR_BODY accident-age window (in days).
|
||||||
|
*
|
||||||
|
* Falls back to {@link DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS} when:
|
||||||
|
* - no client id is supplied,
|
||||||
|
* - the id is malformed,
|
||||||
|
* - the client document is missing,
|
||||||
|
* - the client has no `settings.carBodyAccidentMaxAgeDays` configured,
|
||||||
|
* - or the configured value is not a positive finite number.
|
||||||
|
*/
|
||||||
|
async getCarBodyAccidentMaxAgeDays(
|
||||||
|
clientId?: string | Types.ObjectId | null,
|
||||||
|
): Promise<number> {
|
||||||
|
if (!clientId) return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS;
|
||||||
|
|
||||||
|
const idString = String(clientId);
|
||||||
|
if (!Types.ObjectId.isValid(idString)) {
|
||||||
|
return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await this.clientDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(idString),
|
||||||
|
});
|
||||||
|
const configured = client?.settings?.carBodyAccidentMaxAgeDays;
|
||||||
|
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
|||||||
|
|
||||||
export type ClientDocument = ClientModel & Document;
|
export type ClientDocument = ClientModel & Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-tenant tunables. Add new policy fields here; consumers read them via
|
||||||
|
* `ClientService` with documented defaults so older client documents keep
|
||||||
|
* working when a field is missing.
|
||||||
|
*/
|
||||||
|
export class ClientSettings {
|
||||||
|
/**
|
||||||
|
* Max number of days between the accident and the moment a CAR_BODY blame
|
||||||
|
* file is allowed to be submitted. When unset, consumers fall back to the
|
||||||
|
* system default (see {@link ClientService.getCarBodyAccidentMaxAgeDays}).
|
||||||
|
*/
|
||||||
|
@Prop({ type: Number, required: false })
|
||||||
|
carBodyAccidentMaxAgeDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Schema({ collection: "clients", versionKey: false })
|
@Schema({ collection: "clients", versionKey: false })
|
||||||
export class ClientModel {
|
export class ClientModel {
|
||||||
@Prop({ required: true, unique: true, type: Object })
|
@Prop({ required: true, unique: true, type: Object })
|
||||||
@@ -20,6 +35,9 @@ export class ClientModel {
|
|||||||
|
|
||||||
@Prop({ required: true, unique: false })
|
@Prop({ required: true, unique: false })
|
||||||
clientCode: number;
|
clientCode: number;
|
||||||
|
|
||||||
|
@Prop({ type: ClientSettings, required: false, default: {} })
|
||||||
|
settings?: ClientSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClientDbSchema = SchemaFactory.createForClass(ClientModel);
|
export const ClientDbSchema = SchemaFactory.createForClass(ClientModel);
|
||||||
|
|||||||
@@ -327,6 +327,102 @@ export class RequestManagementService {
|
|||||||
private readonly userAuthService: UserAuthService,
|
private readonly userAuthService: UserAuthService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject CAR_BODY submissions whose accident is older than the per-client
|
||||||
|
* window (see `ClientService.getCarBodyAccidentMaxAgeDays`). The check is
|
||||||
|
* scoped to CAR_BODY because THIRD_PARTY files do not record accidentDate
|
||||||
|
* during the description step.
|
||||||
|
*
|
||||||
|
* `clientId` is best-effort — pass `party.person.clientId`,
|
||||||
|
* `firstPartyDetails.firstPartyClient.clientId`, the SandHub-resolved
|
||||||
|
* `client._id`, etc., whatever is available at the call site. When the
|
||||||
|
* value is missing or invalid the helper falls back to the system default
|
||||||
|
* window so the gate is still enforced.
|
||||||
|
*
|
||||||
|
* Throws `BadRequestException` when the accident instant cannot be parsed,
|
||||||
|
* is in the future, or is older than the configured window.
|
||||||
|
*/
|
||||||
|
private async assertCarBodyAccidentWithinWindow(params: {
|
||||||
|
clientId?: string | Types.ObjectId | null;
|
||||||
|
accidentDate: Date | string | null | undefined;
|
||||||
|
accidentTime?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { clientId, accidentDate, accidentTime } = params;
|
||||||
|
if (accidentDate == null) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "CAR_BODY_ACCIDENT_DATE_REQUIRED",
|
||||||
|
message:
|
||||||
|
"CAR_BODY files require an accident date to enforce the submission window.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const instant = this.parseAccidentInstant(
|
||||||
|
accidentDate,
|
||||||
|
accidentTime ?? undefined,
|
||||||
|
);
|
||||||
|
if (!instant || Number.isNaN(instant.getTime())) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "CAR_BODY_ACCIDENT_DATE_INVALID",
|
||||||
|
message: `Invalid accidentDate/accidentTime: "${String(accidentDate)}"${
|
||||||
|
accidentTime ? ` "${accidentTime}"` : ""
|
||||||
|
}.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ageMs = Date.now() - instant.getTime();
|
||||||
|
if (ageMs < 0) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "CAR_BODY_ACCIDENT_DATE_IN_FUTURE",
|
||||||
|
message: "Accident date cannot be in the future.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAgeDays = await this.clientService.getCarBodyAccidentMaxAgeDays(
|
||||||
|
clientId ?? undefined,
|
||||||
|
);
|
||||||
|
const ageDays = ageMs / (24 * 60 * 60 * 1000);
|
||||||
|
if (ageDays > maxAgeDays) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
code: "CAR_BODY_ACCIDENT_TOO_OLD",
|
||||||
|
message:
|
||||||
|
`CAR_BODY files must be submitted within ${maxAgeDays} day(s) of the accident. ` +
|
||||||
|
`This accident occurred about ${Math.floor(ageDays)} day(s) ago.`,
|
||||||
|
maxAgeDays,
|
||||||
|
ageDays: Math.floor(ageDays),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort parser that combines a date input with an optional `HH:MM`
|
||||||
|
* time. Accepts a `Date`, an ISO datetime string, or an ISO date-only
|
||||||
|
* string so DTO payloads (which currently send `"YYYY-MM-DD"` + `"HH:MM"`)
|
||||||
|
* can be passed through directly. Returns `null` when the result is not a
|
||||||
|
* finite instant.
|
||||||
|
*/
|
||||||
|
private parseAccidentInstant(
|
||||||
|
date: Date | string,
|
||||||
|
time?: string,
|
||||||
|
): Date | null {
|
||||||
|
if (date instanceof Date) {
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = String(date).trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
// Already a full datetime — trust it and ignore the separate time field.
|
||||||
|
if (raw.includes("T") || /\s\d{1,2}:\d{2}/.test(raw)) {
|
||||||
|
const d = new Date(raw);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanTime = (time ?? "").trim();
|
||||||
|
const iso = cleanTime ? `${raw}T${cleanTime}` : raw;
|
||||||
|
const d = new Date(iso);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linked claim cases: block user on damage flow while blame awaits document resend.
|
* Linked claim cases: block user on damage flow while blame awaits document resend.
|
||||||
*/
|
*/
|
||||||
@@ -1226,6 +1322,13 @@ export class RequestManagementService {
|
|||||||
"CAR_BODY description step requires desc, accidentDate, accidentTime, weatherCondition, roadCondition, and lightCondition.",
|
"CAR_BODY description step requires desc, accidentDate, accidentTime, weatherCondition, roadCondition, and lightCondition.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Gate stale CAR_BODY submissions per the party's client window
|
||||||
|
// (falls back to the system default when no client is attached yet).
|
||||||
|
await this.assertCarBodyAccidentWithinWindow({
|
||||||
|
clientId: party.person?.clientId,
|
||||||
|
accidentDate: body.accidentDate,
|
||||||
|
accidentTime: body.accidentTime,
|
||||||
|
});
|
||||||
party.statement.accidentDate = body.accidentDate as any;
|
party.statement.accidentDate = body.accidentDate as any;
|
||||||
party.statement.accidentTime = body.accidentTime;
|
party.statement.accidentTime = body.accidentTime;
|
||||||
(party.statement as any).weatherCondition = body.weatherCondition;
|
(party.statement as any).weatherCondition = body.weatherCondition;
|
||||||
@@ -4506,6 +4609,17 @@ export class RequestManagementService {
|
|||||||
throw new NotFoundException(`Client not found for company: ${clientName}`);
|
throw new NotFoundException(`Client not found for company: ${clientName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gate stale CAR_BODY submissions using the resolved client's window
|
||||||
|
// (V2 expert-initiated path always supplies accidentDate/accidentTime
|
||||||
|
// via formData.expertDescription).
|
||||||
|
if (formData?.expertDescription?.accidentDate) {
|
||||||
|
await this.assertCarBodyAccidentWithinWindow({
|
||||||
|
clientId: (client as any)?._id,
|
||||||
|
accidentDate: formData.expertDescription.accidentDate,
|
||||||
|
accidentTime: formData.expertDescription.accidentTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
const carBodyInfo = await this.mockCarBodyInsuranceInquiry(
|
||||||
requestId,
|
requestId,
|
||||||
firstPartyPlate.plate,
|
firstPartyPlate.plate,
|
||||||
|
|||||||
Reference in New Issue
Block a user