feat(admin): birth date on the customer forms; permission saves limited to known rids (PERS-03, SEC-07)
- PERS-03: «تاریخ تولد» (Jalali DateInput) on the wholesale and retail forms, as legacy. The server accepts birth_date or birth_day, refuses impossible or future dates, clears on '', and returns both names. - SEC-07: setRoles stores only rids in ALL_RIDS or their tree parents. Anything else is dropped with a warning log, so a tick on a legacy-only tree leaf never fails the save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7bb5c2e00f
commit
d91dcf197b
@@ -69,6 +69,23 @@ export type OmdeScope = 'all' | 'banker' | 'shopkeeper' | 'non_verified' | 'arch
|
||||
* `sts_verify_step_one !== 2`, i.e. only an ACCEPTED first step unlocks them.
|
||||
* The new schema models one ordered enum, so these numbers are derived from it.
|
||||
*/
|
||||
/**
|
||||
* PERS-03: the birth date both customer forms collect (legacy bulkCustomer
|
||||
* `birth_date`, singleCustomer `birth_day`). DateInput sends the Gregorian
|
||||
* 'YYYY-MM-DD' of the Jalali date picked; stored at UTC midnight, as the
|
||||
* import stores legacy's. undefined = not sent (leave as is), '' = cleared.
|
||||
*/
|
||||
function birthDateOrNull(raw: unknown): Date | null | undefined {
|
||||
if (raw === undefined) return undefined;
|
||||
if (raw === null || String(raw).trim() === '') return null;
|
||||
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(String(raw).trim());
|
||||
const d = m ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])) : null;
|
||||
if (!d || d.getUTCMonth() !== +m![2] - 1 || d.getUTCFullYear() < 1900 || d > new Date()) {
|
||||
throw new BadRequestException('تاریخ تولد معتبر نیست');
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* A national code as the operator typed it, digits normalised; empty -> null.
|
||||
* The column is optional and unique (2026-10-07): an absent code is NULL,
|
||||
@@ -344,6 +361,9 @@ export class AdminCustomersService {
|
||||
zone: c.zone,
|
||||
pasazh_name: c.pasazhName,
|
||||
mcode: c.memberCode,
|
||||
// Both legacy names: bulkCustomer reads birth_date, singleCustomer birth_day.
|
||||
birth_date: c.birthDate ? c.birthDate.toISOString().slice(0, 10) : null,
|
||||
birth_day: c.birthDate ? c.birthDate.toISOString().slice(0, 10) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -650,6 +670,7 @@ export class AdminCustomersService {
|
||||
nationalCode: null,
|
||||
passwordHash: OTP_ONLY_PASSWORD_HASH,
|
||||
cityId: (data.city_id as string) || null,
|
||||
birthDate: birthDateOrNull(data.birth_day ?? data.birth_date) ?? null,
|
||||
kind: CustomerKind.RETAIL,
|
||||
},
|
||||
});
|
||||
@@ -726,6 +747,7 @@ export class AdminCustomersService {
|
||||
pasazhName: (data.pasazh_name as string) ?? null,
|
||||
zone: (data.zone as string) ?? null,
|
||||
memberCode: (data.mcode as string) ?? null,
|
||||
birthDate: birthDateOrNull(data.birth_date ?? data.birth_day) ?? null,
|
||||
// legacy `image_id` from MyUpload's `ok` payload
|
||||
avatarFileId: (data.image_id as string) || null,
|
||||
// The three document photos the wholesale form collects. These were
|
||||
@@ -802,6 +824,8 @@ export class AdminCustomersService {
|
||||
if (data.pasazh_name !== undefined) patch.pasazhName = String(data.pasazh_name);
|
||||
if (data.zone !== undefined) patch.zone = String(data.zone);
|
||||
if (data.mcode !== undefined) patch.memberCode = String(data.mcode);
|
||||
const birthDate = birthDateOrNull(data.birth_date !== undefined ? data.birth_date : data.birth_day);
|
||||
if (birthDate !== undefined) patch.birthDate = birthDate;
|
||||
if (data.reagent_id !== undefined) {
|
||||
patch.referredByAgent = data.reagent_id
|
||||
? { connect: { id: String(data.reagent_id) } }
|
||||
|
||||
Reference in New Issue
Block a user