feat: port QA/money fixes from tehran_taj (atomic document confirm, validated inputs, expert customer edit/toggle/weights, made-to-order, grid sort/customer column)

Order-revision workflow kept as is: the direct admin line-edit routes were not ported.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
saeed
2026-10-29 18:43:22 +03:30
co-authored by Claude Sonnet 5.5
parent 756ecef6db
commit 3eaecde876
65 changed files with 3810 additions and 216 deletions
@@ -5,6 +5,7 @@ import { PrismaService } from '../../prisma/prisma.service';
import { TableService } from '../admin-table/table.service';
import type { TableQueryDto } from '../admin-table/table-query.dto';
import { faToEn } from '../customer-auth/digits';
import { birthDateOrNull, nationalCodeOrNull } from '../../common/customer-input';
import { OTP_ONLY_PASSWORD_HASH } from '../auth/password-hash';
import { agentSalesWhere, customerSalesWhere } from '../sales-agent/agent-orders';
import { SmsService } from '../sms/sms.service';
@@ -72,33 +73,6 @@ 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,
* never the mobile or '' — two customers without one must not collide, and
* the column must only ever hold a national code.
*/
function nationalCodeOrNull(raw: unknown): string | null {
return faToEn(String(raw ?? '').trim()) || null;
}
function verifyStepOne(step: VerificationStep): number {
switch (step) {
case VerificationStep.UNVERIFIED: