dmvic
Version:
A DMVIC NPM package to manage DMVIC certificate requests
284 lines (262 loc) • 11.5 kB
JavaScript
import { number, object, string } from 'yup';
import {
CLASS_A_CERTIFICATE_TYPE_OPTIONS,
CLASS_D_CERTIFICATE_TYPE_OPTIONS,
CLASS_D_CERTIFICATE_TYPE_OPTIONS_COMMERCIAL,
CLASS_D_CERTIFICATE_TYPE_OPTIONS_WITH_PASSENGERS,
COVER_TYPE_OPTIONS,
INSURERS,
KRA_PIN_REGEX,
MOTOR_CLASS_OPTIONS,
VALUATION_COVER_TYPES_OPTION,
VEHICLE_TYPE_OPTIONS,
} from './constants.mjs';
import { getISOAnnualExpiry } from './standard-date-format.mjs';
const passengerCountRules = {
A: true,
B: true,
D: Object.keys(CLASS_D_CERTIFICATE_TYPE_OPTIONS_WITH_PASSENGERS),
};
const vehicleTonnageRules = {
B: true,
D: Object.keys(CLASS_D_CERTIFICATE_TYPE_OPTIONS_COMMERCIAL),
};
const checkRule = (rules, motorClass, certificateType) =>
rules[motorClass] === true || rules[motorClass]?.includes(certificateType);
const certificateIssuanceSchema = object({
insurer: string().required().oneOf(Object.keys(INSURERS)),
motorClass: string().required().oneOf(Object.values(MOTOR_CLASS_OPTIONS)),
certificateType: string().when('motorClass', {
is: (motorClass) => motorClass === MOTOR_CLASS_OPTIONS.CLASS_A,
then: (certificateTypeSchema) =>
certificateTypeSchema.required().oneOf(Object.keys(CLASS_A_CERTIFICATE_TYPE_OPTIONS)),
otherwise: (certificateTypeSchema) =>
certificateTypeSchema.when('motorClass', {
is: (motorClass) => motorClass === MOTOR_CLASS_OPTIONS.CLASS_D,
then: (certificateTypeSchema) =>
certificateTypeSchema
.required()
.oneOf(Object.keys(CLASS_D_CERTIFICATE_TYPE_OPTIONS)),
otherwise: (certificateTypeSchema) =>
certificateTypeSchema.test(
'must-be-absent',
'certificateType must not be provided for this motorClass',
(value) => value === undefined
),
}),
}),
coverType: string().oneOf(Object.keys(COVER_TYPE_OPTIONS)).required(),
policyHolderFullName: string().strict().required().trim().min(1).max(50),
policyNumber: string().strict().required().trim().min(1).max(50),
commencingDate: string()
.required()
.test('is-valid-date', 'commencingDate must be a valid date', function (value) {
if (!value) return false;
const parsed = new Date(value);
return !isNaN(parsed.getTime());
})
.test('is-not-in-past', 'commencingDate cannot be in the past!', function (value) {
if (!value) return false;
const parsed = new Date(value);
if (isNaN(parsed.getTime())) return false;
const today = new Date();
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const valueStart = new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate());
return valueStart >= todayStart;
})
.test(
'is-within-one-year',
'commencingDate cannot be a year from today!',
function (value) {
if (!value) return false;
const parsed = new Date(value);
if (isNaN(parsed.getTime())) return false;
const maxDateTz = getISOAnnualExpiry();
const maxDate = new Date(
maxDateTz.getFullYear(),
maxDateTz.getMonth(),
maxDateTz.getDate()
);
const targetDate = new Date(
parsed.getFullYear(),
parsed.getMonth(),
parsed.getDate()
);
return maxDate >= targetDate;
}
),
expiringDate: string()
.required('expiringDate is required')
.test('is-valid-date', 'expiringDate must be a valid date', function (value) {
if (!value) return false;
const parsed = new Date(value);
return !isNaN(parsed.getTime());
})
.test(
'is-after-commencing',
'expiringDate must be after or equal to commencingDate!',
function (value) {
if (!value || !this.parent.commencingDate) return false;
const expiringParsed = new Date(value);
const commencingParsed = new Date(this.parent.commencingDate);
if (isNaN(expiringParsed.getTime()) || isNaN(commencingParsed.getTime()))
return false;
const commencingStart = new Date(
commencingParsed.getFullYear(),
commencingParsed.getMonth(),
commencingParsed.getDate()
);
const expiringStart = new Date(
expiringParsed.getFullYear(),
expiringParsed.getMonth(),
expiringParsed.getDate()
);
return expiringStart >= commencingStart;
}
)
.test(
'is-within-one-year',
'expiringDate must be within one year from commencingDate!',
function (value) {
if (!value || !this.parent.commencingDate) return false;
const commencingParsed = new Date(this.parent.commencingDate);
if (isNaN(commencingParsed.getTime())) return false;
const expiringParsed = new Date(value);
if (isNaN(expiringParsed.getTime())) return false;
const maxDate = getISOAnnualExpiry(commencingParsed);
return expiringParsed <= maxDate;
}
),
passengerCount: number().when(['motorClass', 'certificateType'], {
is: (motorClass, certificateType) =>
checkRule(passengerCountRules, motorClass, certificateType),
then: (schema) => schema.required().min(1).max(200),
otherwise: (schema) =>
schema.test(
'must-be-absent',
'passengerCount must not be provided for this motorClass/certificateType',
(value) => value === undefined
),
}),
recipientEmail: string()
.email('recipientEmail must be a valid email address')
.trim()
.required()
.min(1)
.max(100)
.test(
'is-valid-email-address',
'recipientEmail: ${value} is not a valid email address',
(value) => {
const parts = value.split('@');
const [local, domain] = parts;
if (parts.length !== 2) return false;
if (value.includes('+')) return false;
if (value.startsWith('.') || value.endsWith('.')) return false;
if (value.startsWith('-') || value.endsWith('-')) return false;
if (value.includes('..')) return false;
if (local.includes('+')) return false;
if (local.startsWith('.') || local.endsWith('.')) return false;
if (local.startsWith('-') || local.endsWith('-')) return false;
if (!/^[A-Za-z0-9._-]+$/.test(local)) return false;
if (!domain.includes('.')) return false;
return true;
}
),
vehicleYearOfManufacture: number()
.min(1900, 'vehicleYearOfManufacture must be 1900 or later')
.max(new Date().getFullYear(), 'vehicleYearOfManufacture cannot be in the future'),
vehicleRegistrationNumber: string().strict().trim().min(4).max(15),
vehicleEngineNumber: string()
.strict()
.trim()
.min(4)
.max(15)
.test(
'has-alphabets-dashes-integers-only',
'vehicleEngineNumber can only contain alphabets, dashes and integers',
(value) => {
if (value === undefined) return true;
if (value === null) return false;
if (!/^[A-Za-z0-9-]+$/.test(value)) return false;
if (value.startsWith('-') || value.endsWith('-')) return false;
return !value.includes('--');
}
),
vehicleChassisNumber: string()
.strict()
.trim()
.min(4)
.max(20)
.required()
.test(
'has-alphabets-dashes-integers-only',
'chassisNumber can only contain alphabets, dashes and integers',
(value) => {
if (!/^[A-Za-z0-9-]+$/.test(value)) return false;
if (value.startsWith('-') || value.endsWith('-')) return false;
return !value.includes('--');
}
),
vehicleMake: string().strict().trim().min(1).max(50).optional(),
vehicleModel: string().strict().trim().min(1).max(50).optional(),
vehicleValue: number().when('coverType', {
is: (coverType) => Object.keys(VALUATION_COVER_TYPES_OPTION).includes(coverType),
then: (schema) => schema.required().min(1),
otherwise: (schema) =>
schema.test(
'must-be-absent',
'vehicleValue must not be provided for this coverType',
(value) => value === undefined
),
}),
recipientPhoneNumber: number()
.test(
'is-9-digit-phone',
'recipientPhoneNumber must be exactly 9 digits and not start with 0',
(value) => {
if (value === undefined || value === null) return true;
const str = String(value);
return /^[1-9][0-9]{8}$/.test(str);
}
)
.required(),
vehicleBodyType: string().strict().trim().max(50).min(1).required(),
policyHolderKRAPIN: string()
.strict()
.matches(KRA_PIN_REGEX, 'policyHolderKRAPIN must match format: A123456789B')
.optional(),
policyHolderHudumaNumber: string().strict().trim().min(4).max(50).optional(),
vehicleTonnage: number().when(['motorClass', 'certificateType'], {
is: (motorClass, certificateType) =>
checkRule(vehicleTonnageRules, motorClass, certificateType),
then: (schema) => schema.required().min(1).max(31),
otherwise: (schema) =>
schema.test(
'must-be-absent',
'tonnage must not be provided for this motorClass/certificateType',
(value) => value === undefined
),
}),
vehicleType: string().when('motorClass', {
is: (motorClass) => motorClass === MOTOR_CLASS_OPTIONS.CLASS_B,
then: (schema) => schema.required().oneOf(Object.keys(VEHICLE_TYPE_OPTIONS)),
otherwise: (schema) =>
schema.test(
'must-be-absent',
'vehicleType must not be provided unless motorClass is B',
(value) => value === undefined
),
}),
}).test(
'engine-or-registration-exclusive',
'Provide either vehicleEngineNumber or vehicleRegistrationNumber, not both',
function (obj) {
const engine = obj.vehicleEngineNumber;
const reg = obj.vehicleRegistrationNumber;
const validEngine = engine && typeof engine === 'string' && engine.trim().length >= 4;
const validReg = reg && typeof reg === 'string' && reg.trim().length >= 4;
return validEngine ^ validReg;
}
);
export { certificateIssuanceSchema };