added packages into dockploy

This commit is contained in:
2026-09-10 17:24:13 +03:30
parent b15fefe438
commit 59858f06d2
37 changed files with 141995 additions and 845 deletions
+2 -11
View File
@@ -1,16 +1,7 @@
# پیش‌فرض همه چیز ignore؛ فقط آنچه برای Docker لازم است را باز کن
* *
!deploy-bundle.tar.gz !runtime
!prisma-client.tar.gz !runtime/**
!Dockerfile !Dockerfile
!docker-compose.yml !docker-compose.yml
!docker-entrypoint.sh !docker-entrypoint.sh
!dist
!dist/**
!package.json
!package-lock.json
!prisma
!prisma/**
!prisma.config.ts
!.env.example !.env.example
+7 -2
View File
@@ -1,11 +1,16 @@
# compiled output — dist را برای دیپلوی روی گیت می‌فرستیم # خروجی لوکال — روی گیت نرود
# /dist /dist
/dist-docker /dist-docker
/node_modules /node_modules
/build /build
/generated /generated
.deploy-pack .deploy-pack
server-runtime.tgz server-runtime.tgz
deploy-bundle.tar.gz
prisma-client.tar.gz
# runtime باندل‌شده (~23MB) برای Dokploy روی گیت می‌رود
# logs # logs
logs logs
+8 -24
View File
@@ -1,33 +1,17 @@
# bookworm کامل تا OpenSSL آماده باشد (بدون apt روی سرور ایران) # ایمیج اجرا: runtime از گیت می‌آید. npm و node_modules داخل داکر نیست.
FROM node:22-bookworm ARG NODE_IMAGE=node:22-bookworm
FROM ${NODE_IMAGE}
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV TZ=Asia/Tehran ENV TZ=Asia/Tehran
ENV PRISMA_QUERY_ENGINE_LIBRARY=/app/runtime/libquery_engine-debian-openssl-3.0.x.so.node
COPY deploy-bundle.tar.gz /tmp/deploy-bundle.tar.gz COPY runtime ./runtime
RUN tar -xzf /tmp/deploy-bundle.tar.gz -C /app \
&& rm /tmp/deploy-bundle.tar.gz
# هیچ دستور prisma یا npm روی سرور اجرا نمی‌شود؛ همه چیز از قبل ساخته آمده است
ENV CHECKPOINT_DISABLE=1
ENV DO_NOT_TRACK=1
ENV PRISMA_HIDE_UPDATE_MESSAGE=1
COPY package.json package-lock.json prisma.config.ts /app/
COPY prisma /app/prisma
# کلاینت Prisma روی سیستم دولوپر با scripts/pack-prisma-client.sh ساخته می‌شود
COPY prisma-client.tar.gz /tmp/prisma-client.tar.gz
RUN rm -rf /app/node_modules/.prisma/client \
&& mkdir -p /app/node_modules/.prisma \
&& tar -xzf /tmp/prisma-client.tar.gz -C /app/node_modules/.prisma \
&& rm /tmp/prisma-client.tar.gz
# dist آخر باشد تا دیپلوی معمولی فقط همین لایه را دوباره بسازد
COPY dist /app/dist
COPY docker-entrypoint.sh /app/docker-entrypoint.sh COPY docker-entrypoint.sh /app/docker-entrypoint.sh
RUN chmod +x /app/docker-entrypoint.sh RUN chmod +x /app/docker-entrypoint.sh \
&& test -f /app/runtime/server.cjs \
&& test -f /app/runtime/libquery_engine-debian-openssl-3.0.x.so.node
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"] ENTRYPOINT ["./docker-entrypoint.sh"]
+10 -25
View File
@@ -1,32 +1,17 @@
# ایمیج اجرا: dist از گیت می‌آید، فقط dependencyهای production نصب می‌شود # همان Dockerfile: runtime باندل‌شده از گیت، بدون npm
ARG NPM_REGISTRY=https://registry.npmmirror.com ARG NODE_IMAGE=node:22-bookworm
FROM ${NODE_IMAGE}
FROM node:22-alpine
ARG NPM_REGISTRY=https://registry.npmmirror.com
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NPM_CONFIG_REGISTRY=${NPM_REGISTRY} ENV TZ=Asia/Tehran
ENV PRISMA_QUERY_ENGINE_LIBRARY=/app/runtime/libquery_engine-debian-openssl-3.0.x.so.node
COPY package.json package-lock.json ./ COPY runtime ./runtime
COPY prisma ./prisma COPY docker-entrypoint.sh /app/docker-entrypoint.sh
COPY prisma.config.ts ./ RUN chmod +x /app/docker-entrypoint.sh \
&& test -f /app/runtime/server.cjs \
RUN set -eux; \ && test -f /app/runtime/libquery_engine-debian-openssl-3.0.x.so.node
mirror="$(echo "${NPM_REGISTRY}" | sed 's:/*$::')"; \
sed -i \
-e "s|https://registry.npmjs.org|${mirror}|g" \
-e "s|https://registry.npmmirror.com|${mirror}|g" \
package-lock.json; \
npm config set registry "${mirror}"; \
npm ci --omit=dev --no-audit --no-fund; \
npm install prisma ts-node typescript --no-save --no-audit --no-fund --registry="${mirror}"; \
npx prisma generate
# بیلد لوکال که داخل گیت است
COPY dist ./dist
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh
EXPOSE 3000 EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"] ENTRYPOINT ["./docker-entrypoint.sh"]
Binary file not shown.
-191
View File
@@ -1,191 +0,0 @@
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
import { AgentService } from './agent.service';
import { CompleteReminderDto, CreateMeetingDto, CreateReminderDto } from './dto/agent.dto';
export declare class AgentController {
private readonly agentService;
constructor(agentService: AgentService);
categories(): Promise<{
usage: {
id: string;
title: string;
subtitle: string;
}[];
seasons: {
id: string;
title: string;
description: string;
}[];
}>;
profile(user: AuthPrincipal): Promise<{
id: string;
firstName: string;
lastName: string;
code: string;
levelLabel: string;
nextLevelLabel: string;
pointsToNext: number;
levelProgress: number;
phone: string;
customersCount: number;
ordersCount: number;
salesVolumeToman: number;
profitToman: number;
periodLabel: string;
}>;
customers(user: AuthPrincipal, q?: string, status?: string): Promise<{
id: string;
name: string;
contactName: string;
phone: string;
status: string;
avatarLetter: string;
avatarColor: string;
totalPurchaseToman: number;
orderCount: number;
averageOrderToman: number;
levelLabel: string;
firstPurchaseDate: string;
agentCode: string | undefined;
agentName: string | undefined;
}[]>;
customerDetail(user: AuthPrincipal, id: string): Promise<{
customer: {
id: string;
name: string;
contactName: string;
phone: string;
status: string;
avatarLetter: string;
avatarColor: string;
totalPurchaseToman: number;
orderCount: number;
averageOrderToman: number;
levelLabel: string;
firstPurchaseDate: string;
agentCode: string | null | undefined;
agentName: string | undefined;
};
orders: {
id: string;
customerId: string;
title: string;
amountToman: number;
statusLabel: import("@prisma/client").$Enums.OrderStatus;
statusTone: "info";
date: string;
}[];
meetings: {
id: string;
customerId: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string;
date: string;
time: string | undefined;
}[];
}>;
createMeeting(user: AuthPrincipal, dto: CreateMeetingDto): Promise<{
meeting: {
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
agentId: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string | null;
subject: string | null;
content: string | null;
date: string;
time: string | null;
};
reminder: {
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
} | null;
}>;
reminders(user: AuthPrincipal, filter?: string): Promise<{
id: string;
customerId: string;
title: string;
description: string;
time: string;
date: string;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
note: string | undefined;
customerName: string;
contactName: string;
avatarLetter: string;
avatarColor: string;
}[]>;
reminderDetail(user: AuthPrincipal, id: string): Promise<{
reminder: {
id: string;
customerId: string;
title: string;
description: string;
time: string;
date: string;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
note: string | undefined;
};
customer: {
id: string;
name: string;
contactName: string;
phone: string;
avatarLetter: string;
avatarColor: string;
};
history: {
id: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string;
date: string;
}[];
}>;
createReminder(user: AuthPrincipal, dto: CreateReminderDto): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
}>;
completeReminder(user: AuthPrincipal, id: string, dto: CompleteReminderDto): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
}>;
}
-2
View File
@@ -1,2 +0,0 @@
export declare class AgentModule {
}
-191
View File
@@ -1,191 +0,0 @@
import { PrismaService } from '../prisma/prisma.service';
import { CompleteReminderDto, CreateMeetingDto, CreateReminderDto } from './dto/agent.dto';
export declare class AgentService {
private readonly prisma;
constructor(prisma: PrismaService);
private requireAgent;
getProfile(agentId: string): Promise<{
id: string;
firstName: string;
lastName: string;
code: string;
levelLabel: string;
nextLevelLabel: string;
pointsToNext: number;
levelProgress: number;
phone: string;
customersCount: number;
ordersCount: number;
salesVolumeToman: number;
profitToman: number;
periodLabel: string;
}>;
listMyCustomers(agentId: string, q?: string, status?: string): Promise<{
id: string;
name: string;
contactName: string;
phone: string;
status: string;
avatarLetter: string;
avatarColor: string;
totalPurchaseToman: number;
orderCount: number;
averageOrderToman: number;
levelLabel: string;
firstPurchaseDate: string;
agentCode: string | undefined;
agentName: string | undefined;
}[]>;
getCustomerDetail(agentId: string, customerId: string): Promise<{
customer: {
id: string;
name: string;
contactName: string;
phone: string;
status: string;
avatarLetter: string;
avatarColor: string;
totalPurchaseToman: number;
orderCount: number;
averageOrderToman: number;
levelLabel: string;
firstPurchaseDate: string;
agentCode: string | null | undefined;
agentName: string | undefined;
};
orders: {
id: string;
customerId: string;
title: string;
amountToman: number;
statusLabel: import("@prisma/client").$Enums.OrderStatus;
statusTone: "info";
date: string;
}[];
meetings: {
id: string;
customerId: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string;
date: string;
time: string | undefined;
}[];
}>;
createMeeting(agentId: string, dto: CreateMeetingDto): Promise<{
meeting: {
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
agentId: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string | null;
subject: string | null;
content: string | null;
date: string;
time: string | null;
};
reminder: {
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
} | null;
}>;
listReminders(agentId: string, filter?: string): Promise<{
id: string;
customerId: string;
title: string;
description: string;
time: string;
date: string;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
note: string | undefined;
customerName: string;
contactName: string;
avatarLetter: string;
avatarColor: string;
}[]>;
getReminder(agentId: string, reminderId: string): Promise<{
reminder: {
id: string;
customerId: string;
title: string;
description: string;
time: string;
date: string;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
note: string | undefined;
};
customer: {
id: string;
name: string;
contactName: string;
phone: string;
avatarLetter: string;
avatarColor: string;
};
history: {
id: string;
type: import("@prisma/client").$Enums.InteractionType;
title: string;
description: string;
date: string;
}[];
}>;
createReminder(agentId: string, dto: CreateReminderDto): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
}>;
completeReminder(agentId: string, reminderId: string, dto: CompleteReminderDto): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
customerId: string;
title: string;
description: string | null;
date: string;
time: string;
note: string | null;
interactionType: import("@prisma/client").$Enums.InteractionType;
completed: boolean;
completedAt: Date | null;
ownerId: string;
}>;
listCategories(): Promise<{
usage: {
id: string;
title: string;
subtitle: string;
}[];
seasons: {
id: string;
title: string;
description: string;
}[];
}>;
}
-363
View File
@@ -1,363 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
const prisma_service_1 = require("../prisma/prisma.service");
let AgentService = class AgentService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async requireAgent(agentId) {
const agent = await this.prisma.customer.findUnique({
where: { id: agentId },
});
if (!agent || agent.appRole !== client_1.AppUserRole.agent) {
throw new common_1.ForbiddenException('فقط واسطه به این بخش دسترسی دارد');
}
return agent;
}
async getProfile(agentId) {
const agent = await this.requireAgent(agentId);
const [customersCount, ordersCount] = await Promise.all([
this.prisma.customer.count({
where: { assignedAgentId: agentId, isActive: true },
}),
this.prisma.order.count({
where: {
user: { assignedAgentId: agentId },
},
}),
]);
const sales = await this.prisma.order.aggregate({
where: {
user: { assignedAgentId: agentId },
finalAmount: { not: null },
},
_sum: { finalAmount: true },
});
const salesVolume = sales._sum.finalAmount ?? 0;
return {
id: agent.id,
firstName: agent.firstName,
lastName: agent.lastName,
code: agent.agentCode ?? '',
levelLabel: agent.levelLabel ?? 'برنزی',
nextLevelLabel: agent.nextLevelLabel ?? 'نقره‌ای',
pointsToNext: agent.pointsToNext ?? 0,
levelProgress: agent.levelProgress ?? 0,
phone: agent.phone,
customersCount,
ordersCount,
salesVolumeToman: salesVolume,
profitToman: Math.round(salesVolume * 0.1),
periodLabel: '۳۰۰ روز اخیر',
};
}
async listMyCustomers(agentId, q, status) {
await this.requireAgent(agentId);
const where = {
assignedAgentId: agentId,
...(status === 'active' ? { isActive: true } : {}),
...(status === 'inactive' ? { isActive: false } : {}),
...(q
? {
OR: [
{ firstName: { contains: q, mode: 'insensitive' } },
{ lastName: { contains: q, mode: 'insensitive' } },
{ businessName: { contains: q, mode: 'insensitive' } },
{ phone: { contains: q } },
],
}
: {}),
};
const customers = await this.prisma.customer.findMany({
where,
orderBy: { createdAt: 'desc' },
include: {
orders: {
select: { finalAmount: true, id: true },
},
},
});
return customers.map((c) => {
const amounts = c.orders
.map((o) => o.finalAmount ?? 0)
.filter((n) => n > 0);
const total = amounts.reduce((a, b) => a + b, 0);
const orderCount = c.orders.length;
return {
id: c.id,
name: c.businessName || `${c.firstName} ${c.lastName}`,
contactName: `${c.firstName} ${c.lastName}`,
phone: c.phone,
status: !c.isActive ? 'inactive' : orderCount === 0 ? 'new' : 'active',
avatarLetter: c.avatarLetter || c.firstName.charAt(0),
avatarColor: c.avatarColor || '#c4b5fd',
totalPurchaseToman: total,
orderCount,
averageOrderToman: orderCount ? Math.round(total / orderCount) : 0,
levelLabel: c.levelLabel || 'برنزی',
firstPurchaseDate: '—',
agentCode: undefined,
agentName: undefined,
};
});
}
async getCustomerDetail(agentId, customerId) {
await this.requireAgent(agentId);
const customer = await this.prisma.customer.findFirst({
where: { id: customerId, assignedAgentId: agentId },
include: {
orders: {
orderBy: { createdAt: 'desc' },
take: 20,
},
},
});
if (!customer)
throw new common_1.NotFoundException('مشتری یافت نشد');
const agent = await this.prisma.customer.findUnique({
where: { id: agentId },
});
const amounts = customer.orders
.map((o) => o.finalAmount ?? 0)
.filter((n) => n > 0);
const total = amounts.reduce((a, b) => a + b, 0);
const orderCount = customer.orders.length;
const meetings = await this.prisma.meetingInteraction.findMany({
where: { customerId, agentId },
orderBy: { createdAt: 'desc' },
take: 50,
});
return {
customer: {
id: customer.id,
name: customer.businessName || `${customer.firstName} ${customer.lastName}`,
contactName: `${customer.firstName} ${customer.lastName}`,
phone: customer.phone,
status: customer.isActive ? 'active' : 'inactive',
avatarLetter: customer.avatarLetter || customer.firstName.charAt(0),
avatarColor: customer.avatarColor || '#f9a8d4',
totalPurchaseToman: total,
orderCount,
averageOrderToman: orderCount ? Math.round(total / orderCount) : 0,
levelLabel: customer.levelLabel || 'برنزی',
firstPurchaseDate: customer.createdAt.toISOString().slice(0, 10),
agentCode: agent?.agentCode,
agentName: agent ? `${agent.firstName} ${agent.lastName}` : undefined,
},
orders: customer.orders.map((o) => ({
id: o.id,
customerId: customer.id,
title: `سفارش #${o.orderNumber}`,
amountToman: o.finalAmount ?? 0,
statusLabel: o.status,
statusTone: 'info',
date: o.createdAt.toISOString().slice(0, 10),
})),
meetings: meetings.map((m) => ({
id: m.id,
customerId: m.customerId,
type: m.type,
title: m.title,
description: m.description ?? '',
date: m.date,
time: m.time ?? undefined,
})),
};
}
async createMeeting(agentId, dto) {
await this.requireAgent(agentId);
const customer = await this.prisma.customer.findFirst({
where: { id: dto.customerId, assignedAgentId: agentId },
});
if (!customer)
throw new common_1.NotFoundException('مشتری یافت نشد');
const meeting = await this.prisma.meetingInteraction.create({
data: {
agentId,
customerId: dto.customerId,
type: dto.type,
title: dto.title.trim(),
description: dto.description?.trim(),
subject: dto.subject?.trim(),
content: dto.content?.trim(),
date: dto.date,
time: dto.time,
},
});
let reminder = null;
if (dto.createReminder && dto.reminderTitle && dto.reminderDate && dto.reminderTime) {
reminder = await this.prisma.reminder.create({
data: {
ownerId: agentId,
customerId: dto.customerId,
title: dto.reminderTitle.trim(),
description: dto.reminderNote?.trim(),
note: dto.reminderNote?.trim(),
date: dto.reminderDate,
time: dto.reminderTime,
interactionType: dto.type,
},
});
}
return { meeting, reminder };
}
async listReminders(agentId, filter) {
await this.requireAgent(agentId);
const where = { ownerId: agentId };
if (filter === 'past') {
where.completed = true;
}
else if (filter === 'all') {
}
else {
where.completed = false;
}
const reminders = await this.prisma.reminder.findMany({
where,
orderBy: [{ date: 'asc' }, { time: 'asc' }],
include: {
customer: true,
},
});
return reminders.map((r) => ({
id: r.id,
customerId: r.customerId,
title: r.title,
description: r.description ?? '',
time: r.time,
date: r.date,
interactionType: r.interactionType,
completed: r.completed,
note: r.note ?? undefined,
customerName: r.customer.businessName ||
`${r.customer.firstName} ${r.customer.lastName}`,
contactName: `${r.customer.firstName} ${r.customer.lastName}`,
avatarLetter: r.customer.avatarLetter || r.customer.firstName.charAt(0),
avatarColor: r.customer.avatarColor || '#c4b5fd',
}));
}
async getReminder(agentId, reminderId) {
await this.requireAgent(agentId);
const reminder = await this.prisma.reminder.findFirst({
where: { id: reminderId, ownerId: agentId },
include: { customer: true },
});
if (!reminder)
throw new common_1.NotFoundException('یادآور یافت نشد');
const history = await this.prisma.meetingInteraction.findMany({
where: { customerId: reminder.customerId, agentId },
orderBy: { createdAt: 'desc' },
take: 20,
});
return {
reminder: {
id: reminder.id,
customerId: reminder.customerId,
title: reminder.title,
description: reminder.description ?? '',
time: reminder.time,
date: reminder.date,
interactionType: reminder.interactionType,
completed: reminder.completed,
note: reminder.note ?? undefined,
},
customer: {
id: reminder.customer.id,
name: reminder.customer.businessName ||
`${reminder.customer.firstName} ${reminder.customer.lastName}`,
contactName: `${reminder.customer.firstName} ${reminder.customer.lastName}`,
phone: reminder.customer.phone,
avatarLetter: reminder.customer.avatarLetter || reminder.customer.firstName.charAt(0),
avatarColor: reminder.customer.avatarColor || '#f9a8d4',
},
history: history.map((m) => ({
id: m.id,
type: m.type,
title: m.title,
description: m.description ?? '',
date: m.date,
})),
};
}
async createReminder(agentId, dto) {
await this.requireAgent(agentId);
const customer = await this.prisma.customer.findFirst({
where: { id: dto.customerId, assignedAgentId: agentId },
});
if (!customer)
throw new common_1.NotFoundException('مشتری یافت نشد');
return this.prisma.reminder.create({
data: {
ownerId: agentId,
customerId: dto.customerId,
title: dto.title.trim(),
description: dto.description?.trim(),
note: dto.note?.trim(),
date: dto.date,
time: dto.time,
interactionType: dto.interactionType ?? client_1.InteractionType.note,
},
});
}
async completeReminder(agentId, reminderId, dto) {
await this.requireAgent(agentId);
const reminder = await this.prisma.reminder.findFirst({
where: { id: reminderId, ownerId: agentId },
});
if (!reminder)
throw new common_1.NotFoundException('یادآور یافت نشد');
const completed = dto.completed ?? true;
return this.prisma.reminder.update({
where: { id: reminderId },
data: {
completed,
completedAt: completed ? new Date() : null,
},
});
}
async listCategories() {
return {
usage: [
{ id: 'trousers', title: 'پارچه‌های شلواری', subtitle: 'مشاهده محصولات' },
{ id: 'suit', title: 'پارچه‌های کت‌شلواری', subtitle: 'مشاهده محصولات' },
{ id: 'shirt', title: 'پارچه‌های پیراهنی', subtitle: 'مشاهده محصولات' },
],
seasons: [
{
id: 'summer',
title: 'تابستانه',
description: 'سبک، خنک و مناسب فصل گرم',
},
{
id: 'winter',
title: 'زمستانه',
description: 'گرم و مناسب فصل سرد',
},
{
id: 'all_season',
title: 'چهارفصل',
description: 'مناسب استفاده در بیشتر فصل‌های سال',
},
],
};
}
};
exports.AgentService = AgentService;
exports.AgentService = AgentService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AgentService);
//# sourceMappingURL=agent.service.js.map
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
export declare class AppModule {
}
-1
View File
@@ -1 +0,0 @@
export {};
-2
View File
@@ -23,8 +23,6 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.dist dockerfile: Dockerfile.dist
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com}
container_name: fabric_trade_api container_name: fabric_trade_api
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
+3 -29
View File
@@ -4,38 +4,12 @@ set -e
echo "Node: $(node -v)" echo "Node: $(node -v)"
echo "OpenSSL: $(openssl version 2>/dev/null || echo missing)" echo "OpenSSL: $(openssl version 2>/dev/null || echo missing)"
# فقط host را لاگ کن تا سکرت لو نرود
DB_HOST=$(printf '%s' "$DATABASE_URL" | sed -n 's|.*@\([^:/]*\).*|\1|p') DB_HOST=$(printf '%s' "$DATABASE_URL" | sed -n 's|.*@\([^:/]*\).*|\1|p')
echo "DATABASE_URL host: ${DB_HOST:-unknown}" echo "DATABASE_URL host: ${DB_HOST:-unknown}"
if [ ! -x ./node_modules/.bin/prisma ] && [ ! -f ./node_modules/prisma/build/index.js ]; then if [ -z "$PRISMA_QUERY_ENGINE_LIBRARY" ]; then
echo "ERROR: prisma در node_modules نیست. روی سرور بزن:" export PRISMA_QUERY_ENGINE_LIBRARY=/app/runtime/libquery_engine-debian-openssl-3.0.x.so.node
echo " npm install prisma --no-audit --no-fund --registry=https://registry.npmmirror.com"
exit 1
fi
if [ -x ./node_modules/.bin/prisma ]; then
PRISMA="./node_modules/.bin/prisma"
else
PRISMA="node ./node_modules/prisma/build/index.js"
fi
echo "Waiting for database & applying migrations..."
i=0
until $PRISMA migrate deploy; do
i=$((i + 1))
if [ "$i" -ge 30 ]; then
echo "Database/migrate failed after 30 attempts"
exit 1
fi
echo "Retry $i..."
sleep 2
done
if [ "${RUN_SEED:-false}" = "true" ]; then
echo "Seeding database..."
$PRISMA db seed || true
fi fi
echo "Starting API on port ${PORT:-3000}..." echo "Starting API on port ${PORT:-3000}..."
exec node dist/main.js exec node runtime/server.cjs
+2 -1
View File
@@ -12,7 +12,8 @@
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main.js", "start:prod": "node dist/main.js",
"deploy:build": "npm run build && bash scripts/pack-prisma-client.sh", "deploy:build": "node scripts/pack-runtime.mjs",
"pack:docker": "node scripts/pack-runtime.mjs",
"build:deploy": "bash scripts/build-local-dist.sh", "build:deploy": "bash scripts/build-local-dist.sh",
"docker:dist": "docker compose -f docker-compose.dist.yml up -d --build", "docker:dist": "docker compose -f docker-compose.dist.yml up -d --build",
"test": "jest", "test": "jest",
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,469 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateEnum
CREATE TYPE "SettlementType" AS ENUM ('cash', 'check');
-- CreateEnum
CREATE TYPE "CheckRegistrationMode" AS ENUM ('full', 'brief');
-- CreateEnum
CREATE TYPE "OrderStatus" AS ENUM ('cart', 'awaiting_settlement', 'cash_in_progress', 'check_in_progress', 'submitted', 'awaiting_review', 'pricing', 'needs_correction', 'awaiting_settlement_fix', 'cash_invoice_issued', 'awaiting_cash_payment', 'cash_payment_submitted', 'awaiting_cash_confirmation', 'cash_confirmed', 'awaiting_check_review', 'check_needs_fix', 'check_rejected', 'checks_approved', 'final_approved', 'preparing', 'ready_to_ship', 'shipped', 'delivered', 'cancelled', 'rejected', 'expired');
-- CreateEnum
CREATE TYPE "CheckStatus" AS ENUM ('pending_review', 'approved', 'rejected');
-- CreateEnum
CREATE TYPE "CashPaymentStatus" AS ENUM ('pending_review', 'approved', 'rejected', 'expired');
-- CreateEnum
CREATE TYPE "ShipmentStatus" AS ENUM ('preparing', 'ready', 'shipped', 'delivered');
-- CreateEnum
CREATE TYPE "CustomerType" AS ENUM ('wholesale', 'retail');
-- CreateEnum
CREATE TYPE "AdminRole" AS ENUM ('admin', 'sales', 'finance', 'warehouse');
-- CreateEnum
CREATE TYPE "NotificationTone" AS ENUM ('info', 'warning', 'success', 'danger');
-- CreateTable
CREATE TABLE "Company" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"logoUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"maxMeteragePerInvoice" INTEGER NOT NULL DEFAULT 5000,
"cashInvoiceValidityHours" INTEGER NOT NULL DEFAULT 8,
"cashInvoiceValidUntilHour" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Company_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BankAccount" (
"id" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"bankName" TEXT NOT NULL,
"accountHolder" TEXT NOT NULL,
"accountNumber" TEXT NOT NULL,
"cardNumber" TEXT NOT NULL,
"iban" TEXT NOT NULL,
"notes" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BankAccount_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Catalog" (
"id" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"description" TEXT,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Catalog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"catalogId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"color" TEXT NOT NULL,
"description" TEXT,
"minMeterage" DOUBLE PRECISION NOT NULL DEFAULT 30,
"orderMultiplier" DOUBLE PRECISION NOT NULL DEFAULT 10,
"basePrice" DOUBLE PRECISION,
"stock" DOUBLE PRECISION,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Customer" (
"id" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"nationalId" TEXT,
"customerType" "CustomerType" NOT NULL DEFAULT 'wholesale',
"companyId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminUser" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "AdminRole" NOT NULL DEFAULT 'admin',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"lastLoginAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AdminUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OtpChallenge" (
"id" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"code" TEXT NOT NULL,
"firstName" TEXT,
"lastName" TEXT,
"isRegister" BOOLEAN NOT NULL DEFAULT false,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OtpChallenge_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Cart" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"companyId" TEXT,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Cart_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CartItem" (
"id" TEXT NOT NULL,
"cartId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"catalogCode" TEXT NOT NULL,
"productName" TEXT NOT NULL,
"productCode" TEXT NOT NULL,
"color" TEXT NOT NULL,
"meterage" DOUBLE PRECISION NOT NULL,
"unitPrice" DOUBLE PRECISION,
"notes" TEXT,
"companyId" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
CONSTRAINT "CartItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Order" (
"id" TEXT NOT NULL,
"orderNumber" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"companyName" TEXT NOT NULL,
"settlementType" "SettlementType" NOT NULL,
"checkMode" "CheckRegistrationMode",
"status" "OrderStatus" NOT NULL DEFAULT 'awaiting_review',
"totalMeterage" DOUBLE PRECISION NOT NULL DEFAULT 0,
"totalAmount" DOUBLE PRECISION,
"shippingCost" DOUBLE PRECISION,
"totalDiscount" DOUBLE PRECISION,
"finalAmount" DOUBLE PRECISION,
"notes" TEXT,
"checkRassDays" INTEGER,
"checkRassDate" TIMESTAMP(3),
"cashInvoiceExpiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderItem" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"productId" TEXT,
"catalogCode" TEXT NOT NULL,
"productName" TEXT NOT NULL,
"productCode" TEXT,
"color" TEXT NOT NULL,
"requestedMeterage" DOUBLE PRECISION NOT NULL,
"confirmedMeterage" DOUBLE PRECISION,
"unitPrice" DOUBLE PRECISION,
"unitDiscount" DOUBLE PRECISION DEFAULT 0,
"lineTotal" DOUBLE PRECISION,
"notes" TEXT,
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OrderStatusHistory" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"fromStatus" "OrderStatus",
"toStatus" "OrderStatus" NOT NULL,
"changedBy" TEXT NOT NULL,
"note" TEXT,
"reason" TEXT,
"changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrderStatusHistory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Check" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"row" INTEGER NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"dueDate" TIMESTAMP(3) NOT NULL,
"sayadId" TEXT,
"checkNumber" TEXT,
"ownerNationalId" TEXT,
"ownerFirstName" TEXT,
"ownerLastName" TEXT,
"bankName" TEXT,
"branchName" TEXT,
"frontImage" TEXT,
"backImage" TEXT,
"notes" TEXT,
"mode" "CheckRegistrationMode" NOT NULL DEFAULT 'full',
"status" "CheckStatus" NOT NULL DEFAULT 'pending_review',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Check_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CashPaymentReceipt" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"paidAmount" DOUBLE PRECISION,
"transferDate" TIMESTAMP(3),
"transferTime" TEXT,
"trackingNumber" TEXT,
"destinationAccountId" TEXT,
"receiptImage" TEXT,
"notes" TEXT,
"expiresAt" TIMESTAMP(3),
"status" "CashPaymentStatus" NOT NULL DEFAULT 'pending_review',
"submittedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CashPaymentReceipt_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Shipment" (
"id" TEXT NOT NULL,
"orderId" TEXT NOT NULL,
"carrier" TEXT NOT NULL,
"trackingNumber" TEXT,
"meterage" DOUBLE PRECISION NOT NULL,
"status" "ShipmentStatus" NOT NULL DEFAULT 'preparing',
"shippedAt" TIMESTAMP(3),
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Shipment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SystemSettings" (
"id" TEXT NOT NULL DEFAULT 'default',
"maxMeteragePerInvoice" INTEGER NOT NULL DEFAULT 5000,
"cashInvoiceValidityHours" INTEGER NOT NULL DEFAULT 8,
"defaultSettlementCheck" BOOLEAN NOT NULL DEFAULT true,
"defaultSettlementCash" BOOLEAN NOT NULL DEFAULT true,
"smsOrderRegistered" BOOLEAN NOT NULL DEFAULT true,
"smsPaymentReminder" BOOLEAN NOT NULL DEFAULT true,
"smsCheckReview" BOOLEAN NOT NULL DEFAULT true,
"smsShippingUpdate" BOOLEAN NOT NULL DEFAULT true,
"supportPhone" TEXT,
"supportEmail" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SystemSettings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminNotification" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"tone" "NotificationTone" DEFAULT 'info',
"link" TEXT,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdminNotification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InternalMessage" (
"id" TEXT NOT NULL,
"senderName" TEXT NOT NULL,
"subject" TEXT NOT NULL,
"preview" TEXT NOT NULL,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InternalMessage_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Company_slug_key" ON "Company"("slug");
-- CreateIndex
CREATE INDEX "BankAccount_companyId_idx" ON "BankAccount"("companyId");
-- CreateIndex
CREATE INDEX "Catalog_companyId_idx" ON "Catalog"("companyId");
-- CreateIndex
CREATE UNIQUE INDEX "Catalog_companyId_code_key" ON "Catalog"("companyId", "code");
-- CreateIndex
CREATE INDEX "Product_catalogId_idx" ON "Product"("catalogId");
-- CreateIndex
CREATE INDEX "Product_companyId_idx" ON "Product"("companyId");
-- CreateIndex
CREATE UNIQUE INDEX "Product_companyId_code_key" ON "Product"("companyId", "code");
-- CreateIndex
CREATE UNIQUE INDEX "Customer_phone_key" ON "Customer"("phone");
-- CreateIndex
CREATE INDEX "Customer_companyId_idx" ON "Customer"("companyId");
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_phone_key" ON "AdminUser"("phone");
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_email_key" ON "AdminUser"("email");
-- CreateIndex
CREATE INDEX "OtpChallenge_phone_idx" ON "OtpChallenge"("phone");
-- CreateIndex
CREATE INDEX "Cart_userId_idx" ON "Cart"("userId");
-- CreateIndex
CREATE INDEX "CartItem_cartId_idx" ON "CartItem"("cartId");
-- CreateIndex
CREATE UNIQUE INDEX "CartItem_cartId_productId_key" ON "CartItem"("cartId", "productId");
-- CreateIndex
CREATE UNIQUE INDEX "Order_orderNumber_key" ON "Order"("orderNumber");
-- CreateIndex
CREATE INDEX "Order_userId_idx" ON "Order"("userId");
-- CreateIndex
CREATE INDEX "Order_companyId_idx" ON "Order"("companyId");
-- CreateIndex
CREATE INDEX "Order_status_idx" ON "Order"("status");
-- CreateIndex
CREATE INDEX "Order_createdAt_idx" ON "Order"("createdAt");
-- CreateIndex
CREATE INDEX "OrderItem_orderId_idx" ON "OrderItem"("orderId");
-- CreateIndex
CREATE INDEX "OrderStatusHistory_orderId_idx" ON "OrderStatusHistory"("orderId");
-- CreateIndex
CREATE INDEX "Check_orderId_idx" ON "Check"("orderId");
-- CreateIndex
CREATE INDEX "Check_dueDate_idx" ON "Check"("dueDate");
-- CreateIndex
CREATE UNIQUE INDEX "CashPaymentReceipt_orderId_key" ON "CashPaymentReceipt"("orderId");
-- CreateIndex
CREATE UNIQUE INDEX "Shipment_orderId_key" ON "Shipment"("orderId");
-- AddForeignKey
ALTER TABLE "BankAccount" ADD CONSTRAINT "BankAccount_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Catalog" ADD CONSTRAINT "Catalog_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_catalogId_fkey" FOREIGN KEY ("catalogId") REFERENCES "Catalog"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Cart" ADD CONSTRAINT "Cart_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CartItem" ADD CONSTRAINT "CartItem_cartId_fkey" FOREIGN KEY ("cartId") REFERENCES "Cart"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CartItem" ADD CONSTRAINT "CartItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Order" ADD CONSTRAINT "Order_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OrderStatusHistory" ADD CONSTRAINT "OrderStatusHistory_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Check" ADD CONSTRAINT "Check_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CashPaymentReceipt" ADD CONSTRAINT "CashPaymentReceipt_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CashPaymentReceipt" ADD CONSTRAINT "CashPaymentReceipt_destinationAccountId_fkey" FOREIGN KEY ("destinationAccountId") REFERENCES "BankAccount"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Shipment" ADD CONSTRAINT "Shipment_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- شمارنده تلاش ناموفق برای جلوگیری از حدس‌زدن کد ۴ رقمی
ALTER TABLE "OtpChallenge" ADD COLUMN "attempts" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,2 @@
-- روش تسویه دیگر در لحظه ثبت سفارش گرفته نمی‌شود؛ مشتری بعد از قیمت‌گذاری انتخاب می‌کند.
ALTER TABLE "Order" ALTER COLUMN "settlementType" DROP NOT NULL;
@@ -0,0 +1,24 @@
-- اعلان درون‌برنامه‌ای مشتری به‌همراه سوئیچ پیامک «فاکتور آماده شد»
ALTER TABLE "SystemSettings" ADD COLUMN "smsPricingReady" BOOLEAN NOT NULL DEFAULT true;
CREATE TABLE "CustomerNotification" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"orderId" TEXT,
"event" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"tone" "NotificationTone" DEFAULT 'info',
"link" TEXT,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"smsSent" BOOLEAN NOT NULL DEFAULT false,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerNotification_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "CustomerNotification_customerId_isRead_idx" ON "CustomerNotification"("customerId", "isRead");
CREATE INDEX "CustomerNotification_sentAt_idx" ON "CustomerNotification"("sentAt");
ALTER TABLE "CustomerNotification" ADD CONSTRAINT "CustomerNotification_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CustomerNotification" ADD CONSTRAINT "CustomerNotification_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- سقف متراژ شرکت اختیاری می‌شود؛ خالی یعنی از SystemSettings ارث می‌برد.
ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP NOT NULL;
ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP DEFAULT;
@@ -0,0 +1,4 @@
-- یادآوری پایان مهلت پرداخت فقط یک‌بار برای هر فاکتور ارسال می‌شود.
ALTER TABLE "Order" ADD COLUMN "cashReminderSentAt" TIMESTAMP(3);
CREATE INDEX "Order_status_cashInvoiceExpiresAt_idx" ON "Order"("status", "cashInvoiceExpiresAt");
@@ -0,0 +1,15 @@
-- اقلام سفارش شرکت خود را نگه می‌دارند تا یک فاکتور چندشرکتی ممکن شود.
ALTER TABLE "OrderItem" ADD COLUMN "companyId" TEXT;
ALTER TABLE "OrderItem" ADD COLUMN "companyName" TEXT;
UPDATE "OrderItem" AS oi
SET
"companyId" = o."companyId",
"companyName" = o."companyName"
FROM "Order" AS o
WHERE oi."orderId" = o."id";
ALTER TABLE "OrderItem" ALTER COLUMN "companyId" SET NOT NULL;
ALTER TABLE "OrderItem" ALTER COLUMN "companyName" SET NOT NULL;
CREATE INDEX "OrderItem_companyId_idx" ON "OrderItem"("companyId");
@@ -0,0 +1,122 @@
-- AlterEnum
CREATE TYPE "AppUserRole" AS ENUM ('customer', 'agent');
CREATE TYPE "InteractionType" AS ENUM ('in_person', 'call', 'message', 'note');
CREATE TYPE "ProductSeason" AS ENUM ('summer', 'winter', 'all_season');
CREATE TYPE "ProductUsageCategory" AS ENUM ('trousers', 'suit', 'shirt', 'other');
-- AlterTable Product
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "usageCategory" "ProductUsageCategory" NOT NULL DEFAULT 'other';
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "season" "ProductSeason" NOT NULL DEFAULT 'all_season';
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "widthCm" DOUBLE PRECISION;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "composition" TEXT;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "weightGrams" DOUBLE PRECISION;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "weaveType" TEXT;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "colorsJson" JSONB;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "tagsJson" JSONB;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "imageUrlsJson" JSONB;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "subtitle" TEXT;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "rollMeters" DOUBLE PRECISION NOT NULL DEFAULT 50;
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "isNew" BOOLEAN NOT NULL DEFAULT false;
CREATE INDEX IF NOT EXISTS "Product_usageCategory_idx" ON "Product"("usageCategory");
CREATE INDEX IF NOT EXISTS "Product_season_idx" ON "Product"("season");
-- AlterTable Customer
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "appRole" "AppUserRole" NOT NULL DEFAULT 'customer';
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "agentCode" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "businessName" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "levelLabel" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "nextLevelLabel" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "pointsToNext" INTEGER;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "levelProgress" INTEGER DEFAULT 0;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "assignedAgentId" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "avatarLetter" TEXT;
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "avatarColor" TEXT;
CREATE INDEX IF NOT EXISTS "Customer_appRole_idx" ON "Customer"("appRole");
CREATE INDEX IF NOT EXISTS "Customer_assignedAgentId_idx" ON "Customer"("assignedAgentId");
CREATE INDEX IF NOT EXISTS "Customer_agentCode_idx" ON "Customer"("agentCode");
ALTER TABLE "Customer"
ADD CONSTRAINT "Customer_assignedAgentId_fkey"
FOREIGN KEY ("assignedAgentId") REFERENCES "Customer"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
-- MeetingInteraction
CREATE TABLE IF NOT EXISTS "MeetingInteraction" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"type" "InteractionType" NOT NULL DEFAULT 'in_person',
"title" TEXT NOT NULL,
"description" TEXT,
"subject" TEXT,
"content" TEXT,
"date" TEXT NOT NULL,
"time" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MeetingInteraction_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "MeetingInteraction_customerId_idx" ON "MeetingInteraction"("customerId");
CREATE INDEX IF NOT EXISTS "MeetingInteraction_agentId_idx" ON "MeetingInteraction"("agentId");
CREATE INDEX IF NOT EXISTS "MeetingInteraction_date_idx" ON "MeetingInteraction"("date");
ALTER TABLE "MeetingInteraction"
ADD CONSTRAINT "MeetingInteraction_customerId_fkey"
FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "MeetingInteraction"
ADD CONSTRAINT "MeetingInteraction_agentId_fkey"
FOREIGN KEY ("agentId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Reminder
CREATE TABLE IF NOT EXISTS "Reminder" (
"id" TEXT NOT NULL,
"ownerId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"note" TEXT,
"date" TEXT NOT NULL,
"time" TEXT NOT NULL,
"interactionType" "InteractionType" NOT NULL DEFAULT 'note',
"completed" BOOLEAN NOT NULL DEFAULT false,
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Reminder_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "Reminder_ownerId_completed_idx" ON "Reminder"("ownerId", "completed");
CREATE INDEX IF NOT EXISTS "Reminder_customerId_idx" ON "Reminder"("customerId");
CREATE INDEX IF NOT EXISTS "Reminder_date_idx" ON "Reminder"("date");
ALTER TABLE "Reminder"
ADD CONSTRAINT "Reminder_ownerId_fkey"
FOREIGN KEY ("ownerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Reminder"
ADD CONSTRAINT "Reminder_customerId_fkey"
FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ProductFavorite
CREATE TABLE IF NOT EXISTS "ProductFavorite" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProductFavorite_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "ProductFavorite_customerId_productId_key" ON "ProductFavorite"("customerId", "productId");
CREATE INDEX IF NOT EXISTS "ProductFavorite_customerId_idx" ON "ProductFavorite"("customerId");
ALTER TABLE "ProductFavorite"
ADD CONSTRAINT "ProductFavorite_customerId_fkey"
FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProductFavorite"
ADD CONSTRAINT "ProductFavorite_productId_fkey"
FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,149 @@
-- CreateEnum
CREATE TYPE "CatalogRequestStatus" AS ENUM ('new', 'approved', 'ready_to_ship', 'delivered', 'returned', 'cancelled');
-- CreateEnum
CREATE TYPE "CatalogItemHolderType" AS ENUM ('warehouse', 'customer', 'agent');
-- AlterTable SystemSettings
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "minOrderRollsPerProduct" INTEGER NOT NULL DEFAULT 1;
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "showCatalogPublicPrice" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable Brand
CREATE TABLE "Brand" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Brand_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Brand_slug_key" ON "Brand"("slug");
-- CreateTable CustomerLevelDef
CREATE TABLE "CustomerLevelDef" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"label" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"criteriaJson" JSONB,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerLevelDef_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "CustomerLevelDef_code_key" ON "CustomerLevelDef"("code");
-- CreateTable LevelBenefit
CREATE TABLE "LevelBenefit" (
"id" TEXT NOT NULL,
"levelId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
CONSTRAINT "LevelBenefit_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "LevelBenefit_levelId_idx" ON "LevelBenefit"("levelId");
-- AlterTable Customer
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "levelDefId" TEXT;
CREATE INDEX IF NOT EXISTS "Customer_levelDefId_idx" ON "Customer"("levelDefId");
-- CreateTable CustomerBrand
CREATE TABLE "CustomerBrand" (
"customerId" TEXT NOT NULL,
"brandId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CustomerBrand_pkey" PRIMARY KEY ("customerId","brandId")
);
CREATE INDEX "CustomerBrand_brandId_idx" ON "CustomerBrand"("brandId");
-- CreateTable CatalogRequest
CREATE TABLE "CatalogRequest" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"status" "CatalogRequestStatus" NOT NULL DEFAULT 'new',
"note" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CatalogRequest_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "CatalogRequest_customerId_idx" ON "CatalogRequest"("customerId");
CREATE INDEX "CatalogRequest_status_idx" ON "CatalogRequest"("status");
-- CreateTable CatalogRequestItem
CREATE TABLE "CatalogRequestItem" (
"id" TEXT NOT NULL,
"requestId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"quantity" INTEGER NOT NULL DEFAULT 1,
"holderType" "CatalogItemHolderType" NOT NULL DEFAULT 'warehouse',
"holderCustomerId" TEXT,
CONSTRAINT "CatalogRequestItem_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "CatalogRequestItem_requestId_idx" ON "CatalogRequestItem"("requestId");
CREATE INDEX "CatalogRequestItem_productId_idx" ON "CatalogRequestItem"("productId");
-- CreateTable CatalogItemMovement
CREATE TABLE "CatalogItemMovement" (
"id" TEXT NOT NULL,
"itemId" TEXT NOT NULL,
"fromHolder" TEXT NOT NULL,
"toHolder" TEXT NOT NULL,
"note" TEXT,
"movedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CatalogItemMovement_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "CatalogItemMovement_itemId_idx" ON "CatalogItemMovement"("itemId");
-- CreateTable StockNotifyRequest
CREATE TABLE "StockNotifyRequest" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"notifiedAt" TIMESTAMP(3),
CONSTRAINT "StockNotifyRequest_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "StockNotifyRequest_customerId_productId_key" ON "StockNotifyRequest"("customerId", "productId");
CREATE INDEX "StockNotifyRequest_productId_active_idx" ON "StockNotifyRequest"("productId", "active");
-- CreateTable ProductSuggestion
CREATE TABLE "ProductSuggestion" (
"id" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"note" TEXT,
"createdByAdminId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ProductSuggestion_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "ProductSuggestion_customerId_isActive_idx" ON "ProductSuggestion"("customerId", "isActive");
CREATE INDEX "ProductSuggestion_productId_idx" ON "ProductSuggestion"("productId");
-- FKs
ALTER TABLE "Customer" ADD CONSTRAINT "Customer_levelDefId_fkey" FOREIGN KEY ("levelDefId") REFERENCES "CustomerLevelDef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "LevelBenefit" ADD CONSTRAINT "LevelBenefit_levelId_fkey" FOREIGN KEY ("levelId") REFERENCES "CustomerLevelDef"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CustomerBrand" ADD CONSTRAINT "CustomerBrand_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CustomerBrand" ADD CONSTRAINT "CustomerBrand_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CatalogRequest" ADD CONSTRAINT "CatalogRequest_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CatalogRequestItem" ADD CONSTRAINT "CatalogRequestItem_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "CatalogRequest"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "CatalogRequestItem" ADD CONSTRAINT "CatalogRequestItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CatalogItemMovement" ADD CONSTRAINT "CatalogItemMovement_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "CatalogRequestItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "StockNotifyRequest" ADD CONSTRAINT "StockNotifyRequest_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "StockNotifyRequest" ADD CONSTRAINT "StockNotifyRequest_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProductSuggestion" ADD CONSTRAINT "ProductSuggestion_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ProductSuggestion" ADD CONSTRAINT "ProductSuggestion_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "allowAgentPricing" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "agentMinUnitPrice" INTEGER;
@@ -0,0 +1 @@
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "avatarUrl" TEXT;
@@ -0,0 +1 @@
ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "categoryImagesJson" JSONB;
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS "AppBanner" (
"id" TEXT NOT NULL,
"slot" TEXT NOT NULL,
"title" TEXT NOT NULL,
"subtitle" TEXT,
"ctaLabel" TEXT,
"ctaLink" TEXT,
"imageUrl" TEXT,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AppBanner_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "AppBanner_slot_isActive_displayOrder_idx"
ON "AppBanner"("slot", "isActive", "displayOrder");
INSERT INTO "AppBanner" ("id", "slot", "title", "subtitle", "ctaLabel", "ctaLink", "displayOrder", "isActive", "createdAt", "updatedAt")
SELECT 'home-hero-default', 'home_hero', 'کالکشن پاییز و زمستان', 'جدیدترین پارچه‌ها', 'مشاهده کالکشن', '/categories', 0, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1 FROM "AppBanner" WHERE "id" = 'home-hero-default');
+1
View File
@@ -0,0 +1 @@
provider = "postgresql"
+5
View File
@@ -0,0 +1,5 @@
{
"name": "fabric-trade-runtime",
"private": true,
"type": "commonjs"
}
+751
View File
@@ -0,0 +1,751 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum SettlementType {
cash
check
}
enum CheckRegistrationMode {
full
brief
}
enum OrderStatus {
cart
awaiting_settlement
cash_in_progress
check_in_progress
submitted
awaiting_review
pricing
needs_correction
awaiting_settlement_fix
cash_invoice_issued
awaiting_cash_payment
cash_payment_submitted
awaiting_cash_confirmation
cash_confirmed
awaiting_check_review
check_needs_fix
check_rejected
checks_approved
final_approved
preparing
ready_to_ship
shipped
delivered
cancelled
rejected
expired
}
enum CheckStatus {
pending_review
approved
rejected
}
enum CashPaymentStatus {
pending_review
approved
rejected
expired
}
enum ShipmentStatus {
preparing
ready
shipped
delivered
}
enum CustomerType {
wholesale
retail
}
enum AppUserRole {
customer
agent
}
enum InteractionType {
in_person
call
message
note
}
enum ProductSeason {
summer
winter
all_season
}
enum ProductUsageCategory {
trousers
suit
shirt
other
}
enum AdminRole {
admin
sales
finance
warehouse
}
enum NotificationTone {
info
warning
success
danger
}
/// گردش کالیته فیزیکی (§۷ سند)
enum CatalogRequestStatus {
new
approved
ready_to_ship
delivered
returned
cancelled
}
enum CatalogItemHolderType {
warehouse
customer
agent
}
model Company {
id String @id @default(cuid())
name String
slug String @unique
description String?
logoUrl String?
isActive Boolean @default(true)
displayOrder Int @default(0)
/// خالی یعنی از تنظیمات سیستم ارث می‌برد
maxMeteragePerInvoice Int?
cashInvoiceValidityHours Int @default(8)
cashInvoiceValidUntilHour Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bankAccounts BankAccount[]
catalogs Catalog[]
products Product[]
orders Order[]
customers Customer[]
}
model BankAccount {
id String @id @default(cuid())
companyId String
bankName String
accountHolder String
accountNumber String
cardNumber String
iban String
notes String?
isActive Boolean @default(true)
displayOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
cashReceipts CashPaymentReceipt[]
@@index([companyId])
}
model Catalog {
id String @id @default(cuid())
companyId String
name String
code String
description String?
displayOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
products Product[]
@@unique([companyId, code])
@@index([companyId])
}
model Product {
id String @id @default(cuid())
companyId String
catalogId String
name String
code String
color String
description String?
minMeterage Float @default(30)
orderMultiplier Float @default(10)
basePrice Float?
stock Float?
isActive Boolean @default(true)
/// فیلدهای نمایش کاتالوگ موکاپ (اپ کاربر)
usageCategory ProductUsageCategory @default(other)
season ProductSeason @default(all_season)
widthCm Float?
composition String?
weightGrams Float?
weaveType String?
colorsJson Json?
tagsJson Json?
imageUrlsJson Json?
subtitle String?
rollMeters Float @default(50)
isNew Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
catalog Catalog @relation(fields: [catalogId], references: [id], onDelete: Cascade)
cartItems CartItem[]
orderItems OrderItem[]
favorites ProductFavorite[]
catalogRequestItems CatalogRequestItem[]
stockNotifyRequests StockNotifyRequest[]
productSuggestions ProductSuggestion[]
@@unique([companyId, code])
@@index([catalogId])
@@index([companyId])
@@index([usageCategory])
@@index([season])
}
model Customer {
id String @id @default(cuid())
firstName String
lastName String
phone String @unique
nationalId String?
customerType CustomerType @default(wholesale)
/// نقش در اپ کاربر: مشتری عادی یا واسطه
appRole AppUserRole @default(customer)
agentCode String?
businessName String?
levelLabel String?
nextLevelLabel String?
pointsToNext Int?
levelProgress Int? @default(0)
assignedAgentId String?
avatarLetter String?
avatarColor String?
avatarUrl String?
companyId String?
levelDefId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
levelDef CustomerLevelDef? @relation(fields: [levelDefId], references: [id], onDelete: SetNull)
assignedAgent Customer? @relation("AgentCustomers", fields: [assignedAgentId], references: [id], onDelete: SetNull)
agentCustomers Customer[] @relation("AgentCustomers")
carts Cart[]
orders Order[]
notifications CustomerNotification[]
meetings MeetingInteraction[] @relation("MeetingCustomer")
meetingsAsAgent MeetingInteraction[] @relation("MeetingAgent")
reminders Reminder[] @relation("ReminderOwner")
remindersAbout Reminder[] @relation("ReminderCustomer")
favorites ProductFavorite[]
catalogRequests CatalogRequest[]
stockNotifyRequests StockNotifyRequest[]
productSuggestions ProductSuggestion[]
customerBrands CustomerBrand[]
@@index([companyId])
@@index([appRole])
@@index([assignedAgentId])
@@index([agentCode])
@@index([levelDefId])
}
model AdminUser {
id String @id @default(cuid())
fullName String
phone String @unique
email String @unique
passwordHash String
role AdminRole @default(admin)
isActive Boolean @default(true)
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OtpChallenge {
id String @id @default(cuid())
phone String
code String
firstName String?
lastName String?
isRegister Boolean @default(false)
attempts Int @default(0)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([phone])
}
model Cart {
id String @id @default(cuid())
userId String
companyId String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user Customer @relation(fields: [userId], references: [id], onDelete: Cascade)
items CartItem[]
@@index([userId])
}
model CartItem {
id String @id @default(cuid())
cartId String
productId String
catalogCode String
productName String
productCode String
color String
meterage Float
unitPrice Float?
notes String?
companyId String
companyName String
cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
@@unique([cartId, productId])
@@index([cartId])
}
model Order {
id String @id @default(cuid())
orderNumber String @unique
userId String
companyId String
companyName String
// روش تسویه بعد از قیمت‌گذاری ادمین توسط مشتری انتخاب می‌شود
settlementType SettlementType?
checkMode CheckRegistrationMode?
status OrderStatus @default(awaiting_review)
totalMeterage Float @default(0)
totalAmount Float?
shippingCost Float?
totalDiscount Float?
finalAmount Float?
notes String?
checkRassDays Int?
checkRassDate DateTime?
cashInvoiceExpiresAt DateTime?
/// زمان ارسال یادآوری پایان مهلت پرداخت تا دوباره ارسال نشود
cashReminderSentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user Customer @relation(fields: [userId], references: [id], onDelete: Restrict)
company Company @relation(fields: [companyId], references: [id], onDelete: Restrict)
items OrderItem[]
checks Check[]
cashReceipt CashPaymentReceipt?
shipment Shipment?
statusHistory OrderStatusHistory[]
notifications CustomerNotification[]
@@index([userId])
@@index([companyId])
@@index([status])
@@index([createdAt])
@@index([status, cashInvoiceExpiresAt])
}
model OrderItem {
id String @id @default(cuid())
orderId String
productId String?
catalogCode String
productName String
productCode String?
color String
requestedMeterage Float
confirmedMeterage Float?
unitPrice Float?
unitDiscount Float? @default(0)
lineTotal Float?
notes String?
companyId String
companyName String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
@@index([orderId])
@@index([companyId])
}
model OrderStatusHistory {
id String @id @default(cuid())
orderId String
fromStatus OrderStatus?
toStatus OrderStatus
changedBy String
note String?
reason String?
changedAt DateTime @default(now())
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@index([orderId])
}
model Check {
id String @id @default(cuid())
orderId String
row Int
amount Float
dueDate DateTime
sayadId String?
checkNumber String?
ownerNationalId String?
ownerFirstName String?
ownerLastName String?
bankName String?
branchName String?
frontImage String?
backImage String?
notes String?
mode CheckRegistrationMode @default(full)
status CheckStatus @default(pending_review)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@index([orderId])
@@index([dueDate])
}
model CashPaymentReceipt {
id String @id @default(cuid())
orderId String @unique
amount Float
paidAmount Float?
transferDate DateTime?
transferTime String?
trackingNumber String?
destinationAccountId String?
receiptImage String?
notes String?
expiresAt DateTime?
status CashPaymentStatus @default(pending_review)
submittedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
destinationAccount BankAccount? @relation(fields: [destinationAccountId], references: [id], onDelete: SetNull)
}
model Shipment {
id String @id @default(cuid())
orderId String @unique
carrier String
trackingNumber String?
meterage Float
status ShipmentStatus @default(preparing)
shippedAt DateTime?
deliveredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
}
model SystemSettings {
id String @id @default("default")
maxMeteragePerInvoice Int @default(5000)
cashInvoiceValidityHours Int @default(8)
defaultSettlementCheck Boolean @default(true)
defaultSettlementCash Boolean @default(true)
/// حداقل سفارش: تعداد طاقه از هر محصول (§۱۹)
minOrderRollsPerProduct Int @default(1)
/// نمایش قیمت عمومی در کاتالوگ (پیش‌فرض خاموش)
showCatalogPublicPrice Boolean @default(false)
/// اجازه قیمت‌گذاری توسط واسطه (§۱۹)
allowAgentPricing Boolean @default(false)
/// کف قیمت واحد برای قیمت‌گذاری واسطه (ریال) — اختیاری
agentMinUnitPrice Int?
smsOrderRegistered Boolean @default(true)
smsPricingReady Boolean @default(true)
smsPaymentReminder Boolean @default(true)
smsCheckReview Boolean @default(true)
smsShippingUpdate Boolean @default(true)
supportPhone String?
supportEmail String?
/// تصاویر دسته‌های کاربرد/فصل اپ کاربر: { trousers: dataUrl, summer: dataUrl, ... }
categoryImagesJson Json?
updatedAt DateTime @updatedAt
}
/// بنرهای قابل مدیریت بخش‌های اپ کاربر
model AppBanner {
id String @id @default(cuid())
slot String
title String
subtitle String?
ctaLabel String?
ctaLink String?
imageUrl String?
displayOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([slot, isActive, displayOrder])
}
model AdminNotification {
id String @id @default(cuid())
title String
body String
tone NotificationTone? @default(info)
link String?
isRead Boolean @default(false)
sentAt DateTime @default(now())
}
model CustomerNotification {
id String @id @default(cuid())
customerId String
orderId String?
event String
title String
body String
tone NotificationTone? @default(info)
link String?
isRead Boolean @default(false)
smsSent Boolean @default(false)
sentAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
order Order? @relation(fields: [orderId], references: [id], onDelete: SetNull)
@@index([customerId, isRead])
@@index([sentAt])
}
model InternalMessage {
id String @id @default(cuid())
senderName String
subject String
preview String
isRead Boolean @default(false)
sentAt DateTime @default(now())
}
model MeetingInteraction {
id String @id @default(cuid())
customerId String
agentId String
type InteractionType @default(in_person)
title String
description String?
subject String?
content String?
date String
time String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customer Customer @relation("MeetingCustomer", fields: [customerId], references: [id], onDelete: Cascade)
agent Customer @relation("MeetingAgent", fields: [agentId], references: [id], onDelete: Cascade)
@@index([customerId])
@@index([agentId])
@@index([date])
}
model Reminder {
id String @id @default(cuid())
ownerId String
customerId String
title String
description String?
note String?
date String
time String
interactionType InteractionType @default(note)
completed Boolean @default(false)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
owner Customer @relation("ReminderOwner", fields: [ownerId], references: [id], onDelete: Cascade)
customer Customer @relation("ReminderCustomer", fields: [customerId], references: [id], onDelete: Cascade)
@@index([ownerId, completed])
@@index([customerId])
@@index([date])
}
model ProductFavorite {
id String @id @default(cuid())
customerId String
productId String
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@unique([customerId, productId])
@@index([customerId])
}
model Brand {
id String @id @default(cuid())
name String
slug String? @unique
description String?
isActive Boolean @default(true)
displayOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customers CustomerBrand[]
}
model CustomerBrand {
customerId String
brandId String
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
brand Brand @relation(fields: [brandId], references: [id], onDelete: Cascade)
@@id([customerId, brandId])
@@index([brandId])
}
model CustomerLevelDef {
id String @id @default(cuid())
code String @unique
label String
sortOrder Int @default(0)
criteriaJson Json?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
benefits LevelBenefit[]
customers Customer[]
}
model LevelBenefit {
id String @id @default(cuid())
levelId String
title String
description String?
level CustomerLevelDef @relation(fields: [levelId], references: [id], onDelete: Cascade)
@@index([levelId])
}
model CatalogRequest {
id String @id @default(cuid())
customerId String
status CatalogRequestStatus @default(new)
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
items CatalogRequestItem[]
@@index([customerId])
@@index([status])
}
model CatalogRequestItem {
id String @id @default(cuid())
requestId String
productId String
quantity Int @default(1)
holderType CatalogItemHolderType @default(warehouse)
holderCustomerId String?
request CatalogRequest @relation(fields: [requestId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
movements CatalogItemMovement[]
@@index([requestId])
@@index([productId])
}
model CatalogItemMovement {
id String @id @default(cuid())
itemId String
fromHolder String
toHolder String
note String?
movedAt DateTime @default(now())
item CatalogRequestItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
@@index([itemId])
}
model StockNotifyRequest {
id String @id @default(cuid())
customerId String
productId String
active Boolean @default(true)
createdAt DateTime @default(now())
notifiedAt DateTime?
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@unique([customerId, productId])
@@index([productId, active])
}
model ProductSuggestion {
id String @id @default(cuid())
customerId String
productId String
note String?
createdByAdminId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@index([customerId, isActive])
@@index([productId])
}
+140197
View File
File diff suppressed because one or more lines are too long
+92
View File
@@ -0,0 +1,92 @@
import { execSync } from 'node:child_process'
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
rmSync,
statSync,
writeFileSync,
} from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
process.chdir(root)
const runtimeDir = path.join(root, 'runtime')
const engineName = 'libquery_engine-debian-openssl-3.0.x.so.node'
const engineSrc = path.join(root, 'node_modules', '.prisma', 'client', engineName)
function mb(file) {
return `${(statSync(file).size / (1024 * 1024)).toFixed(1)}MB`
}
console.log('==> nest build')
rmSync(path.join(root, 'dist'), { recursive: true, force: true })
rmSync(path.join(root, 'tsconfig.build.tsbuildinfo'), { force: true })
execSync('npx nest build', { stdio: 'inherit' })
if (!existsSync(path.join(root, 'dist', 'main.js'))) {
throw new Error('dist/main.js ساخته نشد')
}
console.log('==> prisma generate (debian engine)')
execSync('npx prisma generate', {
stdio: 'inherit',
env: {
...process.env,
DATABASE_URL:
process.env.DATABASE_URL
|| 'postgresql://build:build@localhost:5432/build?schema=public',
},
})
if (!existsSync(engineSrc)) {
throw new Error(`${engineName} ساخته نشد. binaryTargets را در schema.prisma چک کنید.`)
}
rmSync(runtimeDir, { recursive: true, force: true })
mkdirSync(runtimeDir, { recursive: true })
console.log('==> esbuild bundle (بدون node_modules در ایمیج)')
execSync(
[
'npx --yes esbuild dist/main.js',
'--bundle',
'--platform=node',
'--target=node22',
'--format=cjs',
'--outfile=runtime/server.cjs',
'--keep-names',
'--legal-comments=none',
'--alias:class-transformer/storage=node_modules/class-transformer/cjs/storage.js',
'--external:@nestjs/microservices',
'--external:@nestjs/microservices/microservices-module',
'--external:@nestjs/websockets',
'--external:@nestjs/websockets/socket-module',
].join(' '),
{ stdio: 'inherit' },
)
copyFileSync(engineSrc, path.join(runtimeDir, engineName))
const schemaSrc = path.join(root, 'node_modules', '.prisma', 'client', 'schema.prisma')
if (existsSync(schemaSrc)) {
copyFileSync(schemaSrc, path.join(runtimeDir, 'schema.prisma'))
}
const migrationsSrc = path.join(root, 'prisma', 'migrations')
if (existsSync(migrationsSrc)) {
cpSync(migrationsSrc, path.join(runtimeDir, 'migrations'), { recursive: true })
}
writeFileSync(
path.join(runtimeDir, 'package.json'),
`${JSON.stringify({ name: 'fabric-trade-runtime', private: true, type: 'commonjs' }, null, 2)}\n`,
)
if (!existsSync(path.join(runtimeDir, 'server.cjs'))) {
throw new Error('runtime/server.cjs ساخته نشد')
}
console.log(`runtime/server.cjs ${mb(path.join(runtimeDir, 'server.cjs'))}`)
console.log(`runtime/${engineName} ${mb(path.join(runtimeDir, engineName))}`)
console.log('runtime آماده است. commit و push کنید. مایگریت روی سرور اجرا می‌شود.')
+95
View File
@@ -0,0 +1,95 @@
import { createHash, randomUUID } from 'node:crypto'
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { PrismaClient } from '@prisma/client'
function migrationsDir(): string {
return path.join(process.cwd(), 'runtime', 'migrations')
}
function splitStatements(sql: string): string[] {
return sql
.split(/;\s*(?:\r?\n|$)/)
.map((part) => part.trim())
.filter((part) => part.length > 0 && !part.startsWith('--'))
}
async function waitForDb(prisma: PrismaClient): Promise<void> {
let lastError: unknown
for (let i = 1; i <= 30; i++) {
try {
await prisma.$connect()
await prisma.$queryRaw`SELECT 1`
return
} catch (err) {
lastError = err
console.log(`Waiting for database (${i}/30)...`)
await new Promise((resolve) => setTimeout(resolve, 2000))
}
}
throw lastError
}
export async function applyPendingMigrations(): Promise<void> {
const dir = migrationsDir()
if (!existsSync(dir)) {
console.log('No runtime/migrations; skipping migrate')
return
}
const prisma = new PrismaClient()
try {
await waitForDb(prisma)
await prisma.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"id" VARCHAR(36) PRIMARY KEY NOT NULL,
"checksum" VARCHAR(64) NOT NULL,
"finished_at" TIMESTAMPTZ,
"migration_name" VARCHAR(255) NOT NULL,
"logs" TEXT,
"rolled_back_at" TIMESTAMPTZ,
"started_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
"applied_steps_count" INTEGER NOT NULL DEFAULT 0
)
`)
const applied = await prisma.$queryRaw<Array<{ migration_name: string }>>`
SELECT "migration_name" FROM "_prisma_migrations" WHERE "finished_at" IS NOT NULL
`
const done = new Set(applied.map((row) => row.migration_name))
const folders = readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort()
for (const name of folders) {
if (done.has(name)) continue
const file = path.join(dir, name, 'migration.sql')
if (!existsSync(file)) continue
const sql = readFileSync(file, 'utf8')
const checksum = createHash('sha256').update(sql).digest('hex')
const id = randomUUID()
console.log(`Applying migration ${name}`)
await prisma.$executeRaw`
INSERT INTO "_prisma_migrations" ("id", "checksum", "migration_name", "started_at", "applied_steps_count")
VALUES (${id}, ${checksum}, ${name}, now(), 0)
`
for (const statement of splitStatements(sql)) {
await prisma.$executeRawUnsafe(statement)
}
await prisma.$executeRaw`
UPDATE "_prisma_migrations"
SET "finished_at" = now(), "applied_steps_count" = 1
WHERE "id" = ${id}
`
}
console.log('Migrations up to date')
} finally {
await prisma.$disconnect()
}
}
+5
View File
@@ -3,8 +3,13 @@ import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { json, urlencoded } from 'express'; import { json, urlencoded } from 'express';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { applyPendingMigrations } from './apply-migrations';
async function bootstrap() { async function bootstrap() {
if (process.env.NODE_ENV === 'production') {
await applyPendingMigrations();
}
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
// تصاویر چک و رسید به‌صورت base64 داخل JSON می‌آیند و از سقف پیش‌فرض ۱۰۰KB رد می‌شوند // تصاویر چک و رسید به‌صورت base64 داخل JSON می‌آیند و از سقف پیش‌فرض ۱۰۰KB رد می‌شوند