Files
tehran_taj/apps/api/src/modules/admin-preorders/admin-preorders.service.ts
T
mahmoodandClaude Sonnet 5.5 6514b05d18 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>
2026-10-29 18:09:02 +03:30

169 lines
6.3 KiB
TypeScript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PreOrderStatus, Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { TableService } from '../admin-table/table.service';
import type { TableQueryDto } from '../admin-table/table-query.dto';
/**
* Custom / made-to-order requests — legacy "سفارش های ساختنی".
*
* Legacy served these from CustomOrderController (`POST get-custom-orders`,
* `PUT custom-order/{id}`, `DELETE custom-order/{id}`). The redesigned schema
* models them as PreOrder: a design request that an admin prices and later
* converts into a real, debt-bearing Order.
*/
const PREORDER_STATUS_LABELS: Record<PreOrderStatus, string> = {
PLACED: 'ثبت شده',
ADMIN_APPROVED: 'تایید مدیر',
WORKSHOP_APPROVED: 'تایید کارگاه',
READY_TO_SHIP: 'آماده ارسال',
DELIVERED: 'تحویل شده',
CANCELLED: 'لغو شده',
};
@Injectable()
export class AdminPreOrdersService {
constructor(
private readonly prisma: PrismaService,
private readonly table: TableService,
) {}
private static readonly ALLOWED = [
'id',
'status',
'createdAt',
'updatedAt',
'colorName',
'sizeSpec',
'stoneColor',
'minaColor',
'quantity',
'estimatedWeightGrams',
'estimatedPriceRial',
'customer.fullName',
'customer.mobile',
];
/** Legacy `POST get-custom-orders`. */
async list(query: TableQueryDto) {
this.table.assertNotExport(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' },
});
const [rows, total] = await Promise.all([
this.prisma.preOrder.findMany({
where: where as Prisma.PreOrderWhereInput,
orderBy: orderBy as Prisma.PreOrderOrderByWithRelationInput,
skip,
take,
include: { customer: true, city: true, workshop: true },
}),
this.prisma.preOrder.count({ where: where as Prisma.PreOrderWhereInput }),
]);
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
}
/** Legacy field names (color, size, color_mina, color_stone, ...) preserved. */
private toRow(
p: Prisma.PreOrderGetPayload<{ include: { customer: true; city: true; workshop: true } }>,
) {
return {
id: p.id,
status: p.status,
sts_str_fa: PREORDER_STATUS_LABELS[p.status],
color: p.colorName,
size: p.sizeSpec,
color_mina: p.minaColor,
color_stone: p.stoneColor,
// legacy 'sum' — the pieces asked for
sum: p.quantity,
description: p.description,
designImageUrl: p.designImageUrl,
estimatedWeightGrams: p.estimatedWeightGrams,
estimatedPriceRial: p.estimatedPriceRial,
time_creat_fa: p.createdAt,
time_update_fa: p.updatedAt,
convertedOrderId: p.convertedOrderId,
city: p.city ? { id: p.city.id, title: p.city.name } : null,
workshop: p.workshop ? { id: p.workshop.id, name: p.workshop.name } : null,
customer: {
id: p.customer.id,
// D0 — real name parts
fname: p.customer.firstName ?? p.customer.fullName,
lname: p.customer.lastName ?? '',
name: p.customer.fullName,
fullName: p.customer.fullName,
mobile: p.customer.mobile,
},
};
}
async findOne(id: string) {
const p = await this.prisma.preOrder.findUnique({
where: { id },
include: { customer: true, city: true, workshop: true },
});
if (!p) throw new NotFoundException('سفارش ساختنی یافت نشد');
return this.toRow(p);
}
/** Legacy `PUT custom-order/{id}`. */
async update(id: string, data: Record<string, unknown>) {
const existing = await this.prisma.preOrder.findUnique({ where: { id } });
if (!existing) throw new NotFoundException('سفارش ساختنی یافت نشد');
const patch: Prisma.PreOrderUpdateInput = {};
if (data.status !== undefined) patch.status = data.status as PreOrderStatus;
if (data.color !== undefined) patch.colorName = data.color as string;
if (data.size !== undefined) patch.sizeSpec = data.size as string;
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);
}
if (data.estimatedPriceRial !== undefined) {
patch.estimatedPriceRial = new Prisma.Decimal(data.estimatedPriceRial as string);
}
return this.prisma.preOrder.update({ where: { id }, data: patch });
}
/** Legacy `DELETE custom-order/{id}`. */
async remove(id: string) {
const p = await this.prisma.preOrder.findUnique({ where: { id } });
if (!p) throw new NotFoundException('سفارش ساختنی یافت نشد');
// Once converted into a real Order the design request is part of an
// accounting trail; deleting it would orphan that link. Legacy had no guard.
if (p.convertedOrderId) {
throw new BadRequestException(
'حذف ممکن نیست: این سفارش ساختنی قبلاً به سفارش قطعی تبدیل شده است.',
);
}
await this.prisma.$transaction([
this.prisma.preOrderStatusEvent.deleteMany({ where: { preOrderId: id } }),
this.prisma.preOrder.delete({ where: { id } }),
]);
return { success: true };
}
statusOptions() {
return Object.entries(PREORDER_STATUS_LABELS).map(([value, title]) => ({ value, title }));
}
}