forked from Yara724/api
YARA-899
This commit is contained in:
@@ -7,6 +7,13 @@ import {
|
||||
} from "src/client/dto/create-client.dto";
|
||||
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()
|
||||
export class ClientService {
|
||||
constructor(private readonly clientDbService: ClientDbService) {}
|
||||
@@ -53,4 +60,34 @@ export class ClientService {
|
||||
const list = client.map((element) => new ClientLists(element));
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 })
|
||||
export class ClientModel {
|
||||
@Prop({ required: true, unique: true, type: Object })
|
||||
@@ -20,6 +35,9 @@ export class ClientModel {
|
||||
|
||||
@Prop({ required: true, unique: false })
|
||||
clientCode: number;
|
||||
|
||||
@Prop({ type: ClientSettings, required: false, default: {} })
|
||||
settings?: ClientSettings;
|
||||
}
|
||||
|
||||
export const ClientDbSchema = SchemaFactory.createForClass(ClientModel);
|
||||
|
||||
@@ -327,6 +327,102 @@ export class RequestManagementService {
|
||||
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.
|
||||
*/
|
||||
@@ -1226,6 +1322,13 @@ export class RequestManagementService {
|
||||
"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.accidentTime = body.accidentTime;
|
||||
(party.statement as any).weatherCondition = body.weatherCondition;
|
||||
@@ -4506,6 +4609,17 @@ export class RequestManagementService {
|
||||
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(
|
||||
requestId,
|
||||
firstPartyPlate.plate,
|
||||
|
||||
Reference in New Issue
Block a user