Expand special orders with form details, codes, and SMS copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-12 23:44:27 +03:30
co-authored by Cursor
parent 63253f1877
commit 3cc4c89828
7 changed files with 423 additions and 79 deletions
+201 -20
View File
@@ -1,10 +1,16 @@
import {
BadRequestException,
ConflictException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { Prisma, SpecialOrderCategory, UserRole } from '@prisma/client';
import {
DeliveryType,
Prisma,
SpecialOrderCategory,
UserRole,
} from '@prisma/client';
import { displayName } from '../auth/auth.types';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../sms/sms.service';
@@ -28,20 +34,28 @@ const specialOrderInclude = {
role: true,
},
},
branch: {
select: {
id: true,
name: true,
district: true,
address: true,
},
},
images: {
orderBy: { sortOrder: 'asc' as const },
},
} satisfies Prisma.SpecialOrderInclude;
const DELIVERY_LABEL: Record<DeliveryType, string> = {
pickup: 'دریافت حضوری',
shipping: 'ارسال با پیک',
};
type SpecialOrderRow = Prisma.SpecialOrderGetPayload<{
include: typeof specialOrderInclude;
}>;
const CATEGORY_LABEL: Record<SpecialOrderCategory, string> = {
specialCake: 'کیک ویژه',
specialPastry: 'شیرینی ویژه',
};
@Injectable()
export class SpecialOrdersService {
private readonly logger = new Logger(SpecialOrdersService.name);
@@ -65,6 +79,17 @@ export class SpecialOrdersService {
OR: [
{ name: { contains: q, mode: 'insensitive' as const } },
{ description: { contains: q, mode: 'insensitive' as const } },
{ cakeFlavor: { contains: q, mode: 'insensitive' as const } },
{ filling: { contains: q, mode: 'insensitive' as const } },
{ coating: { contains: q, mode: 'insensitive' as const } },
{ designStyle: { contains: q, mode: 'insensitive' as const } },
{ cakeText: { contains: q, mode: 'insensitive' as const } },
{ decorations: { contains: q, mode: 'insensitive' as const } },
{ branchName: { contains: q, mode: 'insensitive' as const } },
...(Number.isFinite(Number(q.replace(/\D/g, ''))) &&
q.replace(/\D/g, '')
? [{ number: Number(q.replace(/\D/g, '')) }]
: []),
{
user: {
OR: [
@@ -123,12 +148,26 @@ export class SpecialOrdersService {
async create(dto: CreateSpecialOrderDto) {
await this.ensureUser(dto.userId);
const details = await this.resolveDetails(dto);
const row = await this.prisma.specialOrder.create({
data: {
userId: dto.userId,
category: dto.category,
name: dto.name.trim(),
description: dto.description?.trim() ?? '',
description: details.description ?? '',
neededDate: details.neededDate ?? null,
receiveTime: details.receiveTime ?? '',
servings: details.servings ?? null,
cakeFlavor: details.cakeFlavor ?? '',
filling: details.filling ?? '',
baloutSuggestedCombo: details.baloutSuggestedCombo ?? false,
coating: details.coating ?? '',
designStyle: details.designStyle ?? '',
cakeText: details.cakeText ?? '',
decorations: details.decorations ?? '',
deliveryType: details.deliveryType ?? null,
branchId: details.branchId ?? null,
branchName: details.branchName ?? '',
images: {
create: this.imageCreates(dto.images),
},
@@ -162,10 +201,7 @@ export class SpecialOrdersService {
return this.create({
userId,
category: dto.category,
name: dto.name,
description: dto.description,
images: dto.images,
...dto,
});
}
@@ -173,6 +209,8 @@ export class SpecialOrdersService {
await this.ensureExists(id);
if (dto.userId) await this.ensureUser(dto.userId);
const details = await this.resolveDetails(dto, { partial: true });
const row = await this.prisma.$transaction(async (tx) => {
if (dto.images !== undefined) {
await tx.specialOrderImage.deleteMany({
@@ -184,11 +222,7 @@ export class SpecialOrdersService {
where: { id },
data: {
...(dto.userId !== undefined ? { userId: dto.userId } : {}),
...(dto.category !== undefined ? { category: dto.category } : {}),
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
...(dto.description !== undefined
? { description: dto.description.trim() }
: {}),
...details,
...(dto.images !== undefined
? { images: { create: this.imageCreates(dto.images) } }
: {}),
@@ -207,12 +241,11 @@ export class SpecialOrdersService {
}
private async notifyCreated(row: SpecialOrderRow) {
const categoryLabel = CATEGORY_LABEL[row.category];
const customerName = displayName(row.user);
const orderCode = `BL-S-${row.number}`;
const customerPhone = row.user.cellNumber;
const userMessage = `بلوط: سفارش ویژه «${row.name}» (${categoryLabel}) ثبت شد. به‌زودی برای هماهنگی با شما تماس می‌گیریم.`;
const adminMessage = `بلوط: سفارش ویژه جدید\n${categoryLabel} — ${row.name}\nکاربر: ${customerName}\nموبایل: ${customerPhone}`;
const userMessage = `سفارش شما به شماره ی${orderCode} ثبت شد\nاز شما سپاسگزاریم و به زودی با شما تماس خواهیم گرفت`;
const adminMessage = `سفارش ویژه ی جدید به شماره سفارش ${orderCode}`;
try {
await this.sms.send({ to: customerPhone, message: userMessage });
@@ -268,6 +301,128 @@ export class SpecialOrdersService {
}));
}
private async resolveDetails(
dto: {
category?: SpecialOrderCategory;
name?: string;
description?: string;
neededDate?: string | null;
receiveTime?: string;
servings?: number | null;
cakeFlavor?: string;
filling?: string;
baloutSuggestedCombo?: boolean;
coating?: string;
designStyle?: string;
cakeText?: string;
decorations?: string;
deliveryType?: DeliveryType | null;
branchId?: string | null;
},
options?: { partial?: boolean },
) {
const partial = options?.partial ?? false;
const data: {
category?: SpecialOrderCategory;
name?: string;
description?: string;
neededDate?: Date | null;
receiveTime?: string;
servings?: number | null;
cakeFlavor?: string;
filling?: string;
baloutSuggestedCombo?: boolean;
coating?: string;
designStyle?: string;
cakeText?: string;
decorations?: string;
deliveryType?: DeliveryType | null;
branchId?: string | null;
branchName?: string;
} = {};
if (dto.category !== undefined) data.category = dto.category;
if (dto.name !== undefined) data.name = dto.name.trim();
if (dto.description !== undefined) {
data.description = dto.description.trim();
} else if (!partial) {
data.description = '';
}
if (!partial || dto.neededDate !== undefined) {
data.neededDate =
dto.neededDate === null || dto.neededDate === undefined
? null
: new Date(dto.neededDate);
}
if (!partial || dto.receiveTime !== undefined) {
data.receiveTime = dto.receiveTime?.trim() ?? '';
}
if (!partial || dto.servings !== undefined) {
data.servings =
dto.servings === null || dto.servings === undefined
? null
: dto.servings;
}
if (!partial || dto.cakeFlavor !== undefined) {
data.cakeFlavor = dto.cakeFlavor?.trim() ?? '';
}
if (!partial || dto.filling !== undefined) {
data.filling = dto.filling?.trim() ?? '';
}
if (!partial || dto.baloutSuggestedCombo !== undefined) {
data.baloutSuggestedCombo = Boolean(dto.baloutSuggestedCombo);
}
if (!partial || dto.coating !== undefined) {
data.coating = dto.coating?.trim() ?? '';
}
if (!partial || dto.designStyle !== undefined) {
data.designStyle = dto.designStyle?.trim() ?? '';
}
if (!partial || dto.cakeText !== undefined) {
data.cakeText = dto.cakeText?.trim() ?? '';
}
if (!partial || dto.decorations !== undefined) {
data.decorations = dto.decorations?.trim() ?? '';
}
if (!partial || dto.deliveryType !== undefined) {
data.deliveryType =
dto.deliveryType === undefined ? null : dto.deliveryType;
}
if (!partial || dto.branchId !== undefined) {
const branchId =
dto.branchId === undefined ||
dto.branchId === null ||
dto.branchId === ''
? null
: dto.branchId;
if (branchId) {
const branch = await this.prisma.branch.findUnique({
where: { id: branchId },
select: { id: true, name: true },
});
if (!branch) throw new NotFoundException('شعبه یافت نشد');
data.branchId = branch.id;
data.branchName = branch.name;
} else {
data.branchId = null;
data.branchName = '';
}
}
if (
!partial &&
dto.deliveryType === DeliveryType.pickup &&
!data.branchId
) {
throw new BadRequestException(
'انتخاب شعبه برای دریافت حضوری الزامی است',
);
}
return data;
}
private async ensureUser(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
@@ -287,10 +442,36 @@ export class SpecialOrdersService {
private serialize(row: SpecialOrderRow) {
return {
id: row.id,
number: row.number,
code: `BL-S-${row.number}`,
userId: row.userId,
category: row.category,
name: row.name,
description: row.description,
neededDate: row.neededDate?.toISOString() ?? null,
receiveTime: row.receiveTime,
servings: row.servings,
cakeFlavor: row.cakeFlavor,
filling: row.filling,
baloutSuggestedCombo: row.baloutSuggestedCombo,
coating: row.coating,
designStyle: row.designStyle,
cakeText: row.cakeText,
decorations: row.decorations,
deliveryType: row.deliveryType,
deliveryTypeLabel: row.deliveryType
? DELIVERY_LABEL[row.deliveryType]
: null,
branchId: row.branchId,
branchName: row.branchName || row.branch?.name || '',
branch: row.branch
? {
id: row.branch.id,
name: row.branch.name,
district: row.branch.district,
address: row.branch.address,
}
: null,
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
images: row.images.map((image) => ({