fix(money,orders): wage/gram-debt parity, atomic document confirm, expert edit, made-to-order, QA fixes

- Rial wage and gram-debt arithmetic matched to the reference; order corrections keep debt in sync
- Document confirm/un-confirm is a compare-and-swap (no double-applied cache under concurrency)
- Settlement document and expert inputs validated (400 instead of 500)
- Expert: edit / activate-deactivate own customers, adjust raw weights only
- Made-to-order flow in the app, PreOrder.quantity migration
- Order grids: customer column, sort keys; panel select search; pre-order quantity save
- Rebuilt release artifacts

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mahmood
2026-10-29 18:09:02 +03:30
co-authored by Claude Sonnet 5.5
parent 959a272227
commit 6514b05d18
321 changed files with 6984 additions and 2170 deletions
@@ -0,0 +1,68 @@
import { BadRequestException } from '@nestjs/common';
import { PreOrderStatus } from '@prisma/client';
import { AdminPreOrdersService } from './admin-preorders.service';
/** The quantity of a made-to-order request (legacy `sum`) is stored and shown, not a hard-coded 1. */
describe('AdminPreOrdersService — quantity', () => {
const row = {
id: 'p1',
status: PreOrderStatus.PLACED,
colorName: 'طلایی',
sizeSpec: '18',
minaColor: null,
stoneColor: null,
quantity: 7,
description: null,
designImageUrl: null,
estimatedWeightGrams: null,
estimatedPriceRial: null,
convertedOrderId: null,
createdAt: new Date(),
updatedAt: new Date(),
city: null,
workshop: null,
customer: { id: 'c1', firstName: 'علی', lastName: 'رضایی', fullName: 'علی رضایی', mobile: '0912' },
};
function build() {
const updates: any[] = [];
const prisma = {
preOrder: {
findUnique: jest.fn(async () => row),
update: jest.fn(async ({ data }: any) => (updates.push(data), { ...row, ...data })),
},
};
return { service: new AdminPreOrdersService(prisma as never, {} as never), updates };
}
it("shows the request's own quantity as `sum`", async () => {
const { service } = build();
expect(((await service.findOne('p1')) as any).sum).toBe(7);
});
it('lets the panel correct the quantity', async () => {
const { service, updates } = build();
await service.update('p1', { sum: 12 });
expect(updates[0].quantity).toBe(12);
});
it('refuses a quantity that is not a whole number of at least one', async () => {
const { service, updates } = build();
for (const sum of [0, -2, 1.5, 'abc']) {
await expect(service.update('p1', { sum })).rejects.toThrow(BadRequestException);
}
expect(updates).toHaveLength(0);
});
});
describe('AdminPreOrdersService.list — sorting the «تعداد» column', () => {
const { TableService } = jest.requireActual('../admin-table/table.service') as typeof import('../admin-table/table.service');
it('sorts on the stored quantity when the grid asks for its legacy key `sum`', async () => {
const findMany = jest.fn(async () => []);
const prisma = { preOrder: { findMany, count: jest.fn(async () => 0) } };
const service = new AdminPreOrdersService(prisma as never, new TableService() as never);
await service.list({ page: 1, limit: 10, order: '-sum' } as never);
expect((findMany.mock.calls[0] as any)[0].orderBy).toEqual({ quantity: 'desc' });
});
});
@@ -37,6 +37,7 @@ export class AdminPreOrdersService {
'sizeSpec',
'stoneColor',
'minaColor',
'quantity',
'estimatedWeightGrams',
'estimatedPriceRial',
'customer.fullName',
@@ -47,7 +48,10 @@ export class AdminPreOrdersService {
async list(query: TableQueryDto) {
this.table.assertNotExport(query);
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
// The «تعداد» column is keyed `sum` (legacy name); the stored field is `quantity`.
const sorted = query.order && /^[+-]?sum$/.test(query.order) ? { ...query, order: query.order.replace('sum', 'quantity') } : query;
const { where, orderBy, skip, take, page, limit } = this.table.build(sorted, {
searchable: ['customer.fullName', 'customer.mobile'],
allowed: AdminPreOrdersService.ALLOWED,
defaultOrderBy: { createdAt: 'desc' },
@@ -79,8 +83,8 @@ export class AdminPreOrdersService {
size: p.sizeSpec,
color_mina: p.minaColor,
color_stone: p.stoneColor,
// legacy 'sum' (quantity); a PreOrder models a single design request
sum: 1,
// legacy 'sum' — the pieces asked for
sum: p.quantity,
description: p.description,
designImageUrl: p.designImageUrl,
estimatedWeightGrams: p.estimatedWeightGrams,
@@ -123,6 +127,11 @@ export class AdminPreOrdersService {
if (data.color_mina !== undefined) patch.minaColor = data.color_mina as string;
if (data.color_stone !== undefined) patch.stoneColor = data.color_stone as string;
if (data.description !== undefined) patch.description = data.description as string;
if (data.sum !== undefined) {
const sum = Number(data.sum);
if (!Number.isInteger(sum) || sum < 1) throw new BadRequestException('تعداد باید عدد صحیح و حداقل ۱ باشد');
patch.quantity = sum;
}
if (data.estimatedWeightGrams !== undefined) {
patch.estimatedWeightGrams = new Prisma.Decimal(data.estimatedWeightGrams as string);
}