added packages into dockploy
This commit is contained in:
+2
-11
@@ -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
@@ -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
@@ -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
@@ -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.
Vendored
-191
@@ -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;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
export declare class AgentModule {
|
|
||||||
}
|
|
||||||
Vendored
-191
@@ -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;
|
|
||||||
}[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
Vendored
-363
@@ -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
|
|
||||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
export declare class AppModule {
|
|
||||||
}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@@ -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
@@ -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
@@ -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');
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "fabric-trade-runtime",
|
||||||
|
"private": true,
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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 کنید. مایگریت روی سرور اجرا میشود.')
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 رد میشوند
|
||||||
|
|||||||
Reference in New Issue
Block a user