commit b15fefe438576efc7b4e51d7ff5a24fc63852180 Author: safavimohadeseh78 Date: Wed Sep 9 17:08:47 2026 +0330 Initialize the API and add managed app banners. Co-authored-by: Cursor diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fbde41c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# پیش‌فرض همه چیز ignore؛ فقط آنچه برای Docker لازم است را باز کن + +* +!deploy-bundle.tar.gz +!prisma-client.tar.gz +!Dockerfile +!docker-compose.yml +!docker-entrypoint.sh +!dist +!dist/** +!package.json +!package-lock.json +!prisma +!prisma/** +!prisma.config.ts +!.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..611e1d0 --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# این فایل فقط روی سرور/لوکال می‌ماند و داخل گیت commit نمی‌شود. +# ایمیج Docker سکرت ندارد؛ همه تنظیمات از اینجا خوانده می‌شود. + +POSTGRES_USER=fabric +POSTGRES_PASSWORD=fabric123 +POSTGRES_DB=fabric_trade +POSTGRES_PORT=5432 + +# برای ابزارهای روی خود سرور (خارج از Docker) +DATABASE_URL=postgresql://fabric:fabric123@localhost:5432/fabric_trade?schema=public + +API_PORT=8585 +PORT=3000 +# پشت LiteSpeed با URI=/server این را بگذار: +GLOBAL_PREFIX=server/api +API_IMAGE=fabric-trade-api:latest +RUN_SEED=false + +JWT_SECRET=change-me-to-a-long-random-string +JWT_EXPIRES_IN=7d +MOCK_OTP_CODE=1234 +OTP_TTL_MINUTES=5 + +# پیامک OTP (مثل آبشده خانگی — rayegansms / createsms) +SMS_ENABLED=true +SMS_API_URL=http://185.231.112.101/api/createsms +SMS_SECRET_KEY=your-sms-secret-key +SMS_PLATFORM=rayegansms +SMS_HOST=پارچه +SMS_APP_NAME=پارچه + +# سقف حجم بدنه JSON (تصاویر چک و رسید base64 هستند) +BODY_LIMIT=25mb +TZ=Asia/Tehran +NODE_ENV=production + +NPM_REGISTRY=https://registry.npmmirror.com +COMPOSE_BAKE=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d087da --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# compiled output — dist را برای دیپلوی روی گیت می‌فرستیم +# /dist +/dist-docker +/node_modules +/build +/generated +.deploy-pack +server-runtime.tgz + +# logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# tests +/coverage +/.nyc_output + +# IDEs +.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# env +.env +.env.local +.env.*.local + +# local postgres data if any +.pgdata +.tools + +# misc +*.tsbuildinfo diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2254e20 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# bookworm کامل تا OpenSSL آماده باشد (بدون apt روی سرور ایران) +FROM node:22-bookworm +WORKDIR /app + +ENV NODE_ENV=production +ENV TZ=Asia/Tehran + +COPY deploy-bundle.tar.gz /tmp/deploy-bundle.tar.gz +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 +RUN chmod +x /app/docker-entrypoint.sh + +EXPOSE 3000 +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/Dockerfile.bundle b/Dockerfile.bundle new file mode 100644 index 0000000..d8e8be6 --- /dev/null +++ b/Dockerfile.bundle @@ -0,0 +1,17 @@ +# فقط کپی فایل‌های آماده‌شده — بدون npm +FROM node:22-alpine +WORKDIR /app + +ENV NODE_ENV=production + +COPY package.json package-lock.json ./ +COPY node_modules ./node_modules +COPY prisma ./prisma +COPY prisma.config.ts ./ +COPY dist ./dist +COPY docker-entrypoint.sh ./ + +RUN chmod +x docker-entrypoint.sh + +EXPOSE 3000 +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/Dockerfile.dist b/Dockerfile.dist new file mode 100644 index 0000000..2db923d --- /dev/null +++ b/Dockerfile.dist @@ -0,0 +1,32 @@ +# ایمیج اجرا: dist از گیت می‌آید، فقط dependencyهای production نصب می‌شود +ARG NPM_REGISTRY=https://registry.npmmirror.com + +FROM node:22-alpine +ARG NPM_REGISTRY=https://registry.npmmirror.com +WORKDIR /app + +ENV NODE_ENV=production +ENV NPM_CONFIG_REGISTRY=${NPM_REGISTRY} + +COPY package.json package-lock.json ./ +COPY prisma ./prisma +COPY prisma.config.ts ./ + +RUN set -eux; \ + 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 +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e8000d --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Fabric Trade Server + +بک‌اند NestJS + Prisma + PostgreSQL برای اپ کاربر و پنل ادمین تجارت پارچه. + +## پیش‌نیاز + +- Node.js 20+ +- Docker (برای PostgreSQL) + +اگر به Docker دسترسی ندارید: + +```bash +sudo usermod -aG docker "$USER" +# سپس یک‌بار logout/login کنید +``` + +## راه‌اندازی سریع (لوکال بدون Docker برای API) + +```bash +cd fabric_trade_server +docker compose up -d postgres +npm install +npx prisma migrate deploy +npm run prisma:seed +npm run start:dev +``` + +## دیپلوی: بیلد روی سیستم خودت، اجرا روی سرور + +روی سرور هیچ `npm` و هیچ دستور `prisma` اجرا نمی‌شود. سه چیز از قبل ساخته و روی +گیت هستند: `dist`، `prisma-client.tar.gz` (کلاینت Prisma) و +`deploy-bundle.tar.gz` (`node_modules` پروداکشن). + +**۱) روی سیستم خودت:** + +```bash +cd fabric_trade_server +npm run deploy:build +git add dist prisma-client.tar.gz && git commit -m "..." && git push +``` + +**۲) روی سرور:** + +```bash +cd /opt/parche/server +git pull +docker compose up -d --build +docker compose logs -f api +``` + +`Dockerfile` باندل را باز می‌کند، بعد `dist` و `prisma` و کلاینت آماده‌ی Prisma +را روی آن می‌نویسد. `prisma generate` روی هاست پروداکشن چند دقیقه طول می‌کشد، +پس عمداً آنجا اجرا نمی‌شود. + +هر بار که `prisma/schema.prisma` را عوض کردی، `npm run deploy:build` را بزن تا +`prisma-client.tar.gz` هم به‌روز شود؛ وگرنه سرور با کلاینت قدیمی بالا می‌آید. + +**فقط اگر پکیج npm جدید اضافه شد** باندل را دوباره بساز و push کن: + +```bash +bash scripts/pack-bundle.sh +git add deploy-bundle.tar.gz && git commit -m "refresh deploy bundle" && git push +``` + +- `env` سرور از `.env` همان پوشه خوانده می‌شود (`env_file` در compose) +- API روی `127.0.0.1:${API_PORT}` بالا می‌آید (سرور: `8585`) +- مایگریشن موقع استارت کانتینر اجرا می‌شود +- LiteSpeed را Reverse Proxy کنید به `http://127.0.0.1:8585` + +مثال مسیر LiteSpeed: `https://domain.com/api/` → `http://127.0.0.1:3000/api/` + +- API: http://localhost:3000/api +- Swagger: http://localhost:3000/api/docs + +## حساب‌های تست (پس از seed) + +| نقش | ورود | رمز / OTP | +|------|------|-----------| +| ادمین | `09128081392` | `123456` | +| مشتری | `09121234567` | OTP پیامکی (۴ رقم) — با `SMS_ENABLED=false` کد ثابت: `1234` | + +## متغیرهای محیطی + +فایل `.env`: + +``` +DATABASE_URL=postgresql://fabric:fabric123@localhost:5432/fabric_trade?schema=public +JWT_SECRET=fabric-trade-dev-secret-change-me +JWT_EXPIRES_IN=7d +PORT=3000 +MOCK_OTP_CODE=1234 +OTP_TTL_MINUTES=5 +SMS_ENABLED=true +SMS_API_URL=http://185.231.112.101/api/createsms +SMS_SECRET_KEY=... +SMS_PLATFORM=rayegansms +SMS_HOST=پارچه +SMS_APP_NAME=پارچه +``` + +## اسکریپت‌ها + +| دستور | کار | +|--------|-----| +| `npm run db:up` | بالا آوردن Postgres | +| `npm run prisma:migrate` | مایگریشن توسعه | +| `npm run prisma:seed` | دیتای فیک | +| `npm run start:dev` | سرور با watch | +| `npm run db:reset` | ریست کامل دیتابیس | + +## ساختار API (خلاصه) + +### کاربر (Bearer JWT مشتری) +- `POST /api/auth/otp/request` / `verify` +- `GET /api/auth/me` · `PATCH /api/auth/profile` +- `GET /api/companies` · catalogs · products · bank-accounts +- `GET/POST /api/carts` … +- `GET/POST /api/orders` · `POST /api/orders/:id/cash-receipt` + +### ادمین (Bearer JWT ادمین) +- `POST /api/admin/auth/login` +- `GET /api/admin/dashboard` · `/admin/reports` · `/admin/settings` +- CRUD: companies, catalogs, products, customers, users, orders, checks, cash-payments, shipments diff --git a/deploy-bundle.tar.gz b/deploy-bundle.tar.gz new file mode 100644 index 0000000..55e4bac Binary files /dev/null and b/deploy-bundle.tar.gz differ diff --git a/deploy-host-build.sh b/deploy-host-build.sh new file mode 100644 index 0000000..1ed6884 --- /dev/null +++ b/deploy-host-build.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +MIRROR="${NPM_REGISTRY:-https://registry.npmmirror.com}" +MIRROR="${MIRROR%/}" +NODE_VERSION="${NODE_VERSION:-22.14.0}" +NODE_DIR="${PWD}/.tools/node" +NODE_BIN="${NODE_DIR}/bin" + +echo "==> npm registry: $MIRROR" + +# Node پرتابل از میرور (بدون apt و بدون docker pull) +if [ ! -x "${NODE_BIN}/node" ]; then + echo "==> دانلود Node ${NODE_VERSION} از npmmirror..." + mkdir -p .tools + curl -fL --retry 5 --retry-delay 2 \ + "https://npmmirror.com/mirrors/node/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \ + -o /tmp/node.tar.xz + rm -rf "${NODE_DIR}" + mkdir -p .tools + tar -xJf /tmp/node.tar.xz -C .tools + mv ".tools/node-v${NODE_VERSION}-linux-x64" "${NODE_DIR}" + rm -f /tmp/node.tar.xz +fi + +export PATH="${NODE_BIN}:${PATH}" +echo "==> $(node -v) / npm $(npm -v)" + +echo "==> آماده‌سازی package-lock برای میرور..." +sed -i \ + -e "s|https://registry.npmjs.org|${MIRROR}|g" \ + -e "s|https://mirror.iranserver.com/npm|${MIRROR}|g" \ + package-lock.json + +npm config set registry "$MIRROR" +npm config set fetch-retries 5 +npm config set fetch-retry-mintimeout 20000 +npm config set fetch-retry-maxtimeout 120000 +npm config set progress true +npm config set loglevel info + +echo "==> npm ci روی دیسک سرور (سریع‌تر از داخل Docker)..." +npm ci --no-audit --no-fund + +echo "==> prisma generate + build..." +npx prisma generate +npm run build +npm prune --omit=dev +npm install prisma ts-node typescript --no-save --no-audit --no-fund --registry="$MIRROR" + +echo "==> Docker فقط برای اجرا (Postgres + API)..." +export RUN_SEED="${RUN_SEED:-true}" + +# برای بیلد prebuilt به ایمیج پایه node نیاز است؛ اگر نباشد از هاب می‌گیرد +if ! docker image inspect node:22-alpine >/dev/null 2>&1; then + echo "==> pull node:22-alpine برای ایمیج نهایی..." + docker pull node:22-alpine || true +fi + +docker compose -f docker-compose.prebuilt.yml up -d --build + +echo "" +echo "OK → curl http://127.0.0.1:3000/api/health" +echo "logs → docker compose -f docker-compose.prebuilt.yml logs -f api" diff --git a/dist/agent/agent.controller.d.ts b/dist/agent/agent.controller.d.ts new file mode 100644 index 0000000..82366d0 --- /dev/null +++ b/dist/agent/agent.controller.d.ts @@ -0,0 +1,191 @@ +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; + }>; +} diff --git a/dist/agent/agent.module.d.ts b/dist/agent/agent.module.d.ts new file mode 100644 index 0000000..f6a99ae --- /dev/null +++ b/dist/agent/agent.module.d.ts @@ -0,0 +1,2 @@ +export declare class AgentModule { +} diff --git a/dist/agent/agent.service.d.ts b/dist/agent/agent.service.d.ts new file mode 100644 index 0000000..625285a --- /dev/null +++ b/dist/agent/agent.service.d.ts @@ -0,0 +1,191 @@ +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; + }[]; + }>; +} diff --git a/dist/agent/agent.service.js b/dist/agent/agent.service.js new file mode 100644 index 0000000..63e91c6 --- /dev/null +++ b/dist/agent/agent.service.js @@ -0,0 +1,363 @@ +"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 \ No newline at end of file diff --git a/dist/agent/agent.service.js.map b/dist/agent/agent.service.js.map new file mode 100644 index 0000000..22490f3 --- /dev/null +++ b/dist/agent/agent.service.js.map @@ -0,0 +1 @@ +{"version":3,"file":"agent.service.js","sourceRoot":"","sources":["../../src/agent/agent.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAIwB;AACxB,2CAAsE;AACtE,6DAAyD;AAQlD,IAAM,YAAY,GAAlB,MAAM,YAAY;IACM;IAA7B,YAA6B,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE9C,KAAK,CAAC,YAAY,CAAC,OAAe;QACxC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;YAClD,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,oBAAW,CAAC,KAAK,EAAE,CAAC;YAClD,MAAM,IAAI,2BAAkB,CAAC,kCAAkC,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAe;QAC9B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,CAAC,cAAc,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACzB,KAAK,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;aACpD,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;gBACtB,KAAK,EAAE;oBACL,IAAI,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE;iBACnC;aACF,CAAC;SACH,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;YAC9C,KAAK,EAAE;gBACL,IAAI,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE;gBAClC,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE;aAC3B;YACD,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE;SAC5B,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;QAEhD,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE;YAC3B,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,OAAO;YACvC,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,SAAS;YACjD,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,CAAC;YACrC,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,CAAC;YACvC,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,cAAc;YACd,WAAW;YACX,gBAAgB,EAAE,WAAW;YAC7B,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC;YAC1C,WAAW,EAAE,cAAc;SAC5B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,OAAe,EAAE,CAAU,EAAE,MAAe;QAChE,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,KAAK,GAA8B;YACvC,eAAe,EAAE,OAAO;YACxB,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClD,GAAG,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,CAAC;gBACH,CAAC,CAAC;oBACE,EAAE,EAAE;wBACF,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;wBACnD,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;wBAClD,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;wBACtD,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;qBAC3B;iBACF;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACpD,KAAK;YACL,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE;iBACxC;aACF;SACF,CAAC,CAAC;QAEH,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACzB,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM;iBACrB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC;iBAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACxB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;YACjD,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;YACnC,OAAO;gBACL,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,YAAY,IAAI,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,EAAE;gBACtD,WAAW,EAAE,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,EAAE;gBAC3C,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;gBACtE,YAAY,EAAE,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;gBACrD,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,SAAS;gBACvC,kBAAkB,EAAE,KAAK;gBACzB,UAAU;gBACV,iBAAiB,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClE,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,OAAO;gBACnC,iBAAiB,EAAE,GAAG;gBACtB,SAAS,EAAE,SAA+B;gBAC1C,SAAS,EAAE,SAA+B;aAC3C,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,OAAe,EAAE,UAAkB;QACzD,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE;YACnD,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;oBAC9B,IAAI,EAAE,EAAE;iBACT;aACF;SACF,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,gBAAgB,CAAC,CAAC;QAE7D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;YAClD,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM;aAC5B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC;aAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QAE1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,QAAQ,CAAC;YAC7D,KAAK,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE;YAC9B,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,IAAI,EAAE,EAAE;SACT,CAAC,CAAC;QAEH,OAAO;YACL,QAAQ,EAAE;gBACR,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,IAAI,EAAE,QAAQ,CAAC,YAAY,IAAI,GAAG,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,EAAE;gBAC3E,WAAW,EAAE,GAAG,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,EAAE;gBACzD,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU;gBACjD,YAAY,EAAE,QAAQ,CAAC,YAAY,IAAI,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;gBACnE,WAAW,EAAE,QAAQ,CAAC,WAAW,IAAI,SAAS;gBAC9C,kBAAkB,EAAE,KAAK;gBACzB,UAAU;gBACV,iBAAiB,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClE,UAAU,EAAE,QAAQ,CAAC,UAAU,IAAI,OAAO;gBAC1C,iBAAiB,EAAE,QAAQ,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;gBAChE,SAAS,EAAE,KAAK,EAAE,SAAS;gBAC3B,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS;aACtE;YACD,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAClC,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,UAAU,EAAE,QAAQ,CAAC,EAAE;gBACvB,KAAK,EAAE,UAAU,CAAC,CAAC,WAAW,EAAE;gBAChC,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,CAAC;gBAC/B,WAAW,EAAE,CAAC,CAAC,MAAM;gBACrB,UAAU,EAAE,MAAe;gBAC3B,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;aAC7C,CAAC,CAAC;YACH,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC7B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,SAAS;aAC1B,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe,EAAE,GAAqB;QACxD,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE;SACxD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,gBAAgB,CAAC,CAAC;QAE7D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC;YAC1D,IAAI,EAAE;gBACJ,OAAO;gBACP,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;gBACvB,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE;gBACpC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE;gBAC5B,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE;gBAC5B,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI,EAAE,GAAG,CAAC,IAAI;aACf;SACF,CAAC,CAAC;QAEH,IAAI,QAAQ,GAAG,IAAI,CAAC;QACpB,IAAI,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;YACpF,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,IAAI,EAAE;oBACJ,OAAO,EAAE,OAAO;oBAChB,UAAU,EAAE,GAAG,CAAC,UAAU;oBAC1B,KAAK,EAAE,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE;oBAC/B,WAAW,EAAE,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE;oBACrC,IAAI,EAAE,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE;oBAC9B,IAAI,EAAE,GAAG,CAAC,YAAY;oBACtB,IAAI,EAAE,GAAG,CAAC,YAAY;oBACtB,eAAe,EAAE,GAAG,CAAC,IAAI;iBAC1B;aACF,CAAC,CAAC;QACL,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe,EAAE,MAAe;QAClD,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,KAAK,GAA8B,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAC9D,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC;QACzB,CAAC;aAAM,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QAE9B,CAAC;aAAM,CAAC;YAEN,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC;QAC1B,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACpD,KAAK;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;YAC3C,OAAO,EAAE;gBACP,QAAQ,EAAE,IAAI;aACf;SACF,CAAC,CAAC;QAEH,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3B,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;YAChC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,eAAe,EAAE,CAAC,CAAC,eAAe;YAClC,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,SAAS;YACzB,YAAY,EACV,CAAC,CAAC,QAAQ,CAAC,YAAY;gBACvB,GAAG,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE;YAClD,WAAW,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE;YAC7D,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,YAAY,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACvE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,IAAI,SAAS;SACjD,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe,EAAE,UAAkB;QACnD,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE;YAC3C,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC5B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,CAAC,CAAC;QAE9D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,QAAQ,CAAC;YAC5D,KAAK,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,EAAE;YACnD,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,IAAI,EAAE,EAAE;SACT,CAAC,CAAC;QAEH,OAAO;YACL,QAAQ,EAAE;gBACR,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,WAAW,EAAE,QAAQ,CAAC,WAAW,IAAI,EAAE;gBACvC,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,eAAe,EAAE,QAAQ,CAAC,eAAe;gBACzC,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,SAAS;aACjC;YACD,QAAQ,EAAE;gBACR,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE;gBACxB,IAAI,EACF,QAAQ,CAAC,QAAQ,CAAC,YAAY;oBAC9B,GAAG,QAAQ,CAAC,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE;gBAChE,WAAW,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE;gBAC3E,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK;gBAC9B,YAAY,EACV,QAAQ,CAAC,QAAQ,CAAC,YAAY,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;gBACzE,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,WAAW,IAAI,SAAS;aACxD;YACD,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3B,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;gBAChC,IAAI,EAAE,CAAC,CAAC,IAAI;aACb,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAe,EAAE,GAAsB;QAC1D,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE;SACxD,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,gBAAgB,CAAC,CAAC;QAE7D,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjC,IAAI,EAAE;gBACJ,OAAO,EAAE,OAAO;gBAChB,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE;gBACvB,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE;gBACpC,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE;gBACtB,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,eAAe,EAAE,GAAG,CAAC,eAAe,IAAI,wBAAe,CAAC,IAAI;aAC7D;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,OAAe,EACf,UAAkB,EAClB,GAAwB;QAExB,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE;SAC5C,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,CAAC,CAAC;QAC9D,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC;QACxC,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjC,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;YACzB,IAAI,EAAE;gBACJ,SAAS;gBACT,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;aAC3C;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,OAAO;YACL,KAAK,EAAE;gBACL,EAAE,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,gBAAgB,EAAE;gBACzE,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,EAAE,QAAQ,EAAE,gBAAgB,EAAE;gBACxE,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,gBAAgB,EAAE;aACxE;YACD,OAAO,EAAE;gBACP;oBACE,EAAE,EAAE,QAAQ;oBACZ,KAAK,EAAE,UAAU;oBACjB,WAAW,EAAE,0BAA0B;iBACxC;gBACD;oBACE,EAAE,EAAE,QAAQ;oBACZ,KAAK,EAAE,SAAS;oBAChB,WAAW,EAAE,qBAAqB;iBACnC;gBACD;oBACE,EAAE,EAAE,YAAY;oBAChB,KAAK,EAAE,SAAS;oBAChB,WAAW,EAAE,oCAAoC;iBAClD;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AA/WY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAE0B,8BAAa;GADvC,YAAY,CA+WxB"} \ No newline at end of file diff --git a/dist/app.module.d.ts b/dist/app.module.d.ts new file mode 100644 index 0000000..09cdb35 --- /dev/null +++ b/dist/app.module.d.ts @@ -0,0 +1,2 @@ +export declare class AppModule { +} diff --git a/dist/main.d.ts b/dist/main.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/main.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/docker-compose.bundle.yml b/docker-compose.bundle.yml new file mode 100644 index 0000000..2cdd4b6 --- /dev/null +++ b/docker-compose.bundle.yml @@ -0,0 +1,40 @@ +services: + postgres: + image: postgres:16-alpine + container_name: fabric_trade_db + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-fabric} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fabric123} + POSTGRES_DB: ${POSTGRES_DB:-fabric_trade} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - fabric_pg_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: fabric_trade_api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-fabric}:${POSTGRES_PASSWORD:-fabric123}@postgres:5432/${POSTGRES_DB:-fabric_trade}?schema=public + PORT: ${PORT:-3000} + ports: + - '127.0.0.1:${API_PORT:-3000}:3000' + +volumes: + fabric_pg_data: diff --git a/docker-compose.dist.yml b/docker-compose.dist.yml new file mode 100644 index 0000000..f4db510 --- /dev/null +++ b/docker-compose.dist.yml @@ -0,0 +1,42 @@ +services: + postgres: + image: postgres:16-alpine + container_name: fabric_trade_db + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-fabric} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fabric123} + POSTGRES_DB: ${POSTGRES_DB:-fabric_trade} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - fabric_pg_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile.dist + args: + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmmirror.com} + container_name: fabric_trade_api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-fabric}:${POSTGRES_PASSWORD:-fabric123}@postgres:5432/${POSTGRES_DB:-fabric_trade}?schema=public + PORT: ${PORT:-3000} + ports: + - '127.0.0.1:${API_PORT:-3000}:3000' + +volumes: + fabric_pg_data: diff --git a/docker-compose.image.yml b/docker-compose.image.yml new file mode 100644 index 0000000..29076e5 --- /dev/null +++ b/docker-compose.image.yml @@ -0,0 +1,40 @@ +services: + postgres: + image: postgres:16-alpine + container_name: fabric_trade_db + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-fabric} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fabric123} + POSTGRES_DB: ${POSTGRES_DB:-fabric_trade} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - fabric_pg_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] + interval: 5s + timeout: 5s + retries: 10 + + api: + image: ${API_IMAGE:-fabric-trade-api:latest} + container_name: fabric_trade_api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + # سکرت‌ها و تنظیمات از فایل .env روی خود سرور + env_file: + - .env + environment: + # داخل شبکه Docker هاست دیتابیس همیشه "postgres" است + DATABASE_URL: postgresql://${POSTGRES_USER:-fabric}:${POSTGRES_PASSWORD:-fabric123}@postgres:5432/${POSTGRES_DB:-fabric_trade}?schema=public + PORT: ${PORT:-3000} + ports: + - '127.0.0.1:${API_PORT:-3000}:3000' + +volumes: + fabric_pg_data: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..02f73b5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +services: + postgres: + image: postgres:16-alpine + container_name: fabric_trade_db + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-fabric} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fabric123} + POSTGRES_DB: ${POSTGRES_DB:-fabric_trade} + ports: + - '${POSTGRES_PORT:-5432}:5432' + volumes: + - fabric_pg_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: + context: . + dockerfile: Dockerfile + container_name: fabric_trade_api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-fabric}:${POSTGRES_PASSWORD:-fabric123}@postgres:5432/${POSTGRES_DB:-fabric_trade}?schema=public + PORT: ${PORT:-3000} + TZ: ${TZ:-Asia/Tehran} + ports: + - '127.0.0.1:${API_PORT:-8585}:3000' + +volumes: + fabric_pg_data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..627ab9d --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -e + +echo "Node: $(node -v)" +echo "OpenSSL: $(openssl version 2>/dev/null || echo missing)" + +# فقط host را لاگ کن تا سکرت لو نرود +DB_HOST=$(printf '%s' "$DATABASE_URL" | sed -n 's|.*@\([^:/]*\).*|\1|p') +echo "DATABASE_URL host: ${DB_HOST:-unknown}" + +if [ ! -x ./node_modules/.bin/prisma ] && [ ! -f ./node_modules/prisma/build/index.js ]; then + echo "ERROR: prisma در node_modules نیست. روی سرور بزن:" + 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 + +echo "Starting API on port ${PORT:-3000}..." +exec node dist/main.js diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4e9f827 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..31dfe09 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "builder": "tsc", + "deleteOutDir": true, + "webpack": false + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4cd94d5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11074 @@ +{ + "name": "fabric_trade_server", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fabric_trade_server", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.4.6", + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "dotenv": "^17.4.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "prisma": "^6.19.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.27.tgz", + "integrity": "sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@nestjs/cli": { + "version": "11.0.24", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.24.tgz", + "integrity": "sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@angular-devkit/schematics-cli": "19.2.27", + "@inquirer/prompts": "7.10.1", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.2.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "13.0.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.9.3", + "webpack": "5.106.2", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.28.tgz", + "integrity": "sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==", + "license": "MIT", + "dependencies": { + "file-type": "21.3.4", + "iterare": "1.2.1", + "load-esm": "1.0.3", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.4.tgz", + "integrity": "sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==", + "license": "MIT", + "dependencies": { + "dotenv": "17.4.1", + "dotenv-expand": "12.0.3", + "lodash": "4.18.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.28.tgz", + "integrity": "sha512-06m63xIRj8+l8uOeh/8LnYupGubkyu4f+bPKIadaSui6vK9KpXgoz7HveT1yOVLcEt0M0oCOEW5EuEXZkEmBBQ==", + "license": "MIT", + "dependencies": { + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz", + "integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==", + "license": "MIT", + "dependencies": { + "cors": "2.8.6", + "express": "5.2.1", + "multer": "2.2.0", + "path-to-regexp": "8.4.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "6.1.3", + "resolved": "https://registry.npmmirror.com/@nestjs/schedule/-/schedule-6.1.3.tgz", + "integrity": "sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==", + "license": "MIT", + "dependencies": { + "cron": "4.4.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "typescript": ">=4.8.2" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.24", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.4.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.6.tgz", + "integrity": "sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "@nestjs/mapped-types": "2.1.1", + "js-yaml": "5.2.1", + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.8" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/swagger/node_modules/js-yaml": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", + "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.28", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.28.tgz", + "integrity": "sha512-B+VgRxeLaH7jkOMgAyUP3N3rpFlisQ7JRxixRbgHvG6a0VgKbbkNSofKExexCgKmQQak80undb3+2kE1lUBmRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/luxon/-/luxon-3.7.4.tgz", + "integrity": "sha512-V536ZAd6ZJztrrBlLcDFaaZrXNAL2E5uGmssWf/dpSiLkmkLScXUYhUBnWPmtW+cIqnNHzf6//TCMpIc9SCRRQ==", + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.11", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.11.tgz", + "integrity": "sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.1.tgz", + "integrity": "sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.401", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz", + "integrity": "sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-check/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.10.tgz", + "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-esm": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", + "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/minimizer-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", + "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", + "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "picomatch": "^4.0.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..62df158 --- /dev/null +++ b/package.json @@ -0,0 +1,104 @@ +{ + "name": "fabric_trade_server", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "rm -rf dist tsconfig.build.tsbuildinfo && nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main.js", + "deploy:build": "npm run build && bash scripts/pack-prisma-client.sh", + "build:deploy": "bash scripts/build-local-dist.sh", + "docker:dist": "docker compose -f docker-compose.dist.yml up -d --build", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "test:smoke": "node scripts/smoke-order-flow.mjs", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "prisma db seed", + "db:up": "docker compose up -d postgres", + "db:down": "docker compose down", + "db:reset": "prisma migrate reset --force", + "docker:up": "docker compose up -d --build", + "docker:down": "docker compose down", + "docker:logs": "docker compose logs -f api", + "docker:export": "bash scripts/build-and-export-image.sh" + }, + "prisma": { + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.4.6", + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "dotenv": "^17.4.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "prisma": "^6.19.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/prisma-client.tar.gz b/prisma-client.tar.gz new file mode 100644 index 0000000..08af907 Binary files /dev/null and b/prisma-client.tar.gz differ diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..5e17cd6 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + seed: 'ts-node --compiler-options {"module":"CommonJS"} prisma/seed.ts', + }, + engine: "classic", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/migrations/20241025000000_init/migration.sql b/prisma/migrations/20241025000000_init/migration.sql new file mode 100644 index 0000000..2ef6deb --- /dev/null +++ b/prisma/migrations/20241025000000_init/migration.sql @@ -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; + diff --git a/prisma/migrations/20260808000000_otp_attempts/migration.sql b/prisma/migrations/20260808000000_otp_attempts/migration.sql new file mode 100644 index 0000000..c790fe3 --- /dev/null +++ b/prisma/migrations/20260808000000_otp_attempts/migration.sql @@ -0,0 +1,2 @@ +-- شمارنده تلاش ناموفق برای جلوگیری از حدس‌زدن کد ۴ رقمی +ALTER TABLE "OtpChallenge" ADD COLUMN "attempts" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20260809000000_settlement_after_pricing/migration.sql b/prisma/migrations/20260809000000_settlement_after_pricing/migration.sql new file mode 100644 index 0000000..44d3598 --- /dev/null +++ b/prisma/migrations/20260809000000_settlement_after_pricing/migration.sql @@ -0,0 +1,2 @@ +-- روش تسویه دیگر در لحظه ثبت سفارش گرفته نمی‌شود؛ مشتری بعد از قیمت‌گذاری انتخاب می‌کند. +ALTER TABLE "Order" ALTER COLUMN "settlementType" DROP NOT NULL; diff --git a/prisma/migrations/20260809010000_customer_notifications/migration.sql b/prisma/migrations/20260809010000_customer_notifications/migration.sql new file mode 100644 index 0000000..eeb346b --- /dev/null +++ b/prisma/migrations/20260809010000_customer_notifications/migration.sql @@ -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; diff --git a/prisma/migrations/20260809020000_company_meterage_inherits_settings/migration.sql b/prisma/migrations/20260809020000_company_meterage_inherits_settings/migration.sql new file mode 100644 index 0000000..2ba700e --- /dev/null +++ b/prisma/migrations/20260809020000_company_meterage_inherits_settings/migration.sql @@ -0,0 +1,3 @@ +-- سقف متراژ شرکت اختیاری می‌شود؛ خالی یعنی از SystemSettings ارث می‌برد. +ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP NOT NULL; +ALTER TABLE "Company" ALTER COLUMN "maxMeteragePerInvoice" DROP DEFAULT; diff --git a/prisma/migrations/20260809030000_cash_reminder_sent_at/migration.sql b/prisma/migrations/20260809030000_cash_reminder_sent_at/migration.sql new file mode 100644 index 0000000..571cbee --- /dev/null +++ b/prisma/migrations/20260809030000_cash_reminder_sent_at/migration.sql @@ -0,0 +1,4 @@ +-- یادآوری پایان مهلت پرداخت فقط یک‌بار برای هر فاکتور ارسال می‌شود. +ALTER TABLE "Order" ADD COLUMN "cashReminderSentAt" TIMESTAMP(3); + +CREATE INDEX "Order_status_cashInvoiceExpiresAt_idx" ON "Order"("status", "cashInvoiceExpiresAt"); diff --git a/prisma/migrations/20260815100000_order_item_company/migration.sql b/prisma/migrations/20260815100000_order_item_company/migration.sql new file mode 100644 index 0000000..8e12c8a --- /dev/null +++ b/prisma/migrations/20260815100000_order_item_company/migration.sql @@ -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"); diff --git a/prisma/migrations/20260908130000_app_user_crm_catalog/migration.sql b/prisma/migrations/20260908130000_app_user_crm_catalog/migration.sql new file mode 100644 index 0000000..e627e92 --- /dev/null +++ b/prisma/migrations/20260908130000_app_user_crm_catalog/migration.sql @@ -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; diff --git a/prisma/migrations/20260908180000_admin_non_crm/migration.sql b/prisma/migrations/20260908180000_admin_non_crm/migration.sql new file mode 100644 index 0000000..80e7cec --- /dev/null +++ b/prisma/migrations/20260908180000_admin_non_crm/migration.sql @@ -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; diff --git a/prisma/migrations/20260909120000_agent_pricing_settings/migration.sql b/prisma/migrations/20260909120000_agent_pricing_settings/migration.sql new file mode 100644 index 0000000..0c4fbba --- /dev/null +++ b/prisma/migrations/20260909120000_agent_pricing_settings/migration.sql @@ -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; diff --git a/prisma/migrations/20260909160000_customer_avatar_url/migration.sql b/prisma/migrations/20260909160000_customer_avatar_url/migration.sql new file mode 100644 index 0000000..d42cff6 --- /dev/null +++ b/prisma/migrations/20260909160000_customer_avatar_url/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "avatarUrl" TEXT; diff --git a/prisma/migrations/20260909180000_category_images/migration.sql b/prisma/migrations/20260909180000_category_images/migration.sql new file mode 100644 index 0000000..4ee1eed --- /dev/null +++ b/prisma/migrations/20260909180000_category_images/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "SystemSettings" ADD COLUMN IF NOT EXISTS "categoryImagesJson" JSONB; diff --git a/prisma/migrations/20260909190000_app_banners/migration.sql b/prisma/migrations/20260909190000_app_banners/migration.sql new file mode 100644 index 0000000..fb33418 --- /dev/null +++ b/prisma/migrations/20260909190000_app_banners/migration.sql @@ -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'); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2fe25d8 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..7f350f7 --- /dev/null +++ b/prisma/schema.prisma @@ -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]) +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..6a3d1d4 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,882 @@ +import { + PrismaClient, + AdminRole, + AppUserRole, + CustomerType, + OrderStatus, + SettlementType, + CheckRegistrationMode, + CheckStatus, + CashPaymentStatus, + ShipmentStatus, + InteractionType, + ProductSeason, + ProductUsageCategory, +} from '@prisma/client'; +import * as bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Seeding fabric_trade database...'); + + await prisma.productFavorite.deleteMany(); + await prisma.reminder.deleteMany(); + await prisma.meetingInteraction.deleteMany(); + await prisma.orderStatusHistory.deleteMany(); + await prisma.cashPaymentReceipt.deleteMany(); + await prisma.check.deleteMany(); + await prisma.shipment.deleteMany(); + await prisma.orderItem.deleteMany(); + await prisma.order.deleteMany(); + await prisma.cartItem.deleteMany(); + await prisma.cart.deleteMany(); + await prisma.otpChallenge.deleteMany(); + await prisma.customerNotification.deleteMany(); + await prisma.product.deleteMany(); + await prisma.catalog.deleteMany(); + await prisma.bankAccount.deleteMany(); + await prisma.customer.deleteMany(); + await prisma.company.deleteMany(); + await prisma.adminUser.deleteMany(); + await prisma.adminNotification.deleteMany(); + await prisma.internalMessage.deleteMany(); + await prisma.systemSettings.deleteMany(); + + const passwordHash = await bcrypt.hash('123456', 10); + + await prisma.adminUser.create({ + data: { + id: 'usr-1', + fullName: 'مدیر سیستم', + phone: '09128081392', + email: 'admin@parche.ir', + passwordHash, + role: AdminRole.admin, + isActive: true, + }, + }); + + await prisma.company.createMany({ + data: [ + { + id: 'company-par', + name: 'شرکت پَر', + slug: 'par', + description: 'تولیدکننده پارچه‌های باکیفیت', + isActive: true, + displayOrder: 1, + maxMeteragePerInvoice: null, + cashInvoiceValidityHours: 8, + cashInvoiceValidUntilHour: 17, + }, + { + id: 'company-fam-baft', + name: 'فام‌بافت', + slug: 'fam-baft', + description: 'عرضه پارچه‌های تخصصی', + isActive: true, + displayOrder: 2, + maxMeteragePerInvoice: null, + cashInvoiceValidityHours: 8, + cashInvoiceValidUntilHour: 17, + }, + ], + }); + + await prisma.bankAccount.createMany({ + data: [ + { + id: 'ba-par-1', + companyId: 'company-par', + bankName: 'بانک ملی', + accountHolder: 'شرکت پَر', + accountNumber: '0123456789', + cardNumber: '6037-9912-3456-7890', + iban: 'IR120170000000012345678901', + isActive: true, + displayOrder: 1, + }, + { + id: 'ba-par-2', + companyId: 'company-par', + bankName: 'بانک صادرات', + accountHolder: 'شرکت پَر', + accountNumber: '9876543210', + cardNumber: '6037-6912-3456-7891', + iban: 'IR620190000000098765432101', + isActive: true, + displayOrder: 2, + }, + { + id: 'ba-fam-1', + companyId: 'company-fam-baft', + bankName: 'بانک ملت', + accountHolder: 'فام‌بافت', + accountNumber: '1122334455', + cardNumber: '6104-3378-9012-3456', + iban: 'IR450120000000011223344556', + isActive: true, + displayOrder: 1, + }, + ], + }); + + await prisma.catalog.createMany({ + data: [ + { id: 'cat-377', companyId: 'company-par', name: 'کالیته ۳۷۷', code: '377', displayOrder: 1, isActive: true }, + { id: 'cat-378', companyId: 'company-par', name: 'کالیته ۳۷۸', code: '378', displayOrder: 2, isActive: true }, + { id: 'cat-379', companyId: 'company-par', name: 'کالیته ۳۷۹', code: '379', displayOrder: 3, isActive: true }, + { id: 'cat-380', companyId: 'company-par', name: 'کالیته ۳۸۰', code: '380', displayOrder: 4, isActive: true }, + { id: 'cat-381', companyId: 'company-par', name: 'کالیته ۳۸۱', code: '381', displayOrder: 5, isActive: true }, + { id: 'cat-fam-401', companyId: 'company-fam-baft', name: 'کالیته ۴۰۱', code: '401', displayOrder: 1, isActive: true }, + { id: 'cat-fam-402', companyId: 'company-fam-baft', name: 'کالیته ۴۰۲', code: '402', displayOrder: 2, isActive: true }, + ], + }); + + await prisma.product.createMany({ + data: [ + { + id: 'prod-377-blue', + companyId: 'company-par', + catalogId: 'cat-377', + name: 'فاستونی ایتالیایی', + code: '377-BL', + color: 'آبی', + minMeterage: 30, + orderMultiplier: 10, + basePrice: 185000, + stock: 10000, + isActive: true, + usageCategory: ProductUsageCategory.suit, + season: ProductSeason.winter, + widthCm: 150, + composition: '۸۰٪ پشم، ۲۰٪ پلی‌استر', + weightGrams: 280, + weaveType: 'تویل', + colorsJson: ['#1e3a8a', '#111827', '#6b7280', '#92400e'], + tagsJson: ['کت‌شلواری', 'زمستانه', 'رسمی'], + subtitle: 'مناسب کت و شلوار رسمی', + rollMeters: 50, + isNew: true, + }, + { + id: 'prod-377-black', + companyId: 'company-par', + catalogId: 'cat-377', + name: 'پارچه مشکی رسمی', + code: '377-BK', + color: 'مشکی', + minMeterage: 30, + orderMultiplier: 10, + basePrice: 192000, + stock: 8000, + isActive: true, + usageCategory: ProductUsageCategory.trousers, + season: ProductSeason.all_season, + widthCm: 150, + composition: '۷۰٪ پشم، ۳۰٪ پلی‌استر', + weightGrams: 260, + weaveType: 'ساده', + colorsJson: ['#111827', '#374151'], + tagsJson: ['شلواری', 'چهارفصل'], + subtitle: 'شلوار رسمی روزانه', + rollMeters: 50, + isNew: true, + }, + { + id: 'prod-378-blue', + companyId: 'company-par', + catalogId: 'cat-378', + name: 'پیراهنی نخی آبی', + code: '378-BL', + color: 'آبی', + minMeterage: 50, + orderMultiplier: 10, + basePrice: 200000, + stock: 5000, + isActive: true, + usageCategory: ProductUsageCategory.shirt, + season: ProductSeason.summer, + widthCm: 145, + composition: '۱۰۰٪ پنبه', + weightGrams: 120, + weaveType: 'پاپلین', + colorsJson: ['#3b82f6', '#93c5fd', '#ffffff'], + tagsJson: ['پیراهنی', 'تابستانه'], + subtitle: 'خنک و سبک', + rollMeters: 40, + isNew: true, + }, + { + id: 'prod-378-black', + companyId: 'company-par', + catalogId: 'cat-378', + name: 'پیراهنی مشکی', + code: '378-BK', + color: 'مشکی', + minMeterage: 50, + orderMultiplier: 10, + basePrice: 205000, + stock: 5000, + isActive: true, + usageCategory: ProductUsageCategory.shirt, + season: ProductSeason.all_season, + widthCm: 145, + composition: '۹۵٪ پنبه، ۵٪ الاستان', + weightGrams: 130, + colorsJson: ['#111827', '#4b5563'], + tagsJson: ['پیراهنی'], + subtitle: 'پیراهن اداری', + rollMeters: 40, + isNew: false, + }, + { + id: 'prod-378-blue-white', + companyId: 'company-par', + catalogId: 'cat-378', + name: 'آبی پشت سفید', + code: '378-BLW', + color: 'آبی پشت سفید', + minMeterage: 50, + orderMultiplier: 10, + basePrice: 210000, + stock: 3000, + isActive: true, + usageCategory: ProductUsageCategory.suit, + season: ProductSeason.summer, + widthCm: 150, + composition: '۵۵٪ پشم، ۴۵٪ پلی‌استر', + weightGrams: 220, + colorsJson: ['#60a5fa', '#f8fafc'], + tagsJson: ['کت‌شلواری', 'تابستانه'], + subtitle: 'سبک تابستانه', + rollMeters: 50, + isNew: false, + }, + { + id: 'prod-378-black-white', + companyId: 'company-par', + catalogId: 'cat-378', + name: 'مشکی پشت سفید', + code: '378-BKW', + color: 'مشکی پشت سفید', + minMeterage: 50, + orderMultiplier: 10, + basePrice: 215000, + stock: 3000, + isActive: true, + usageCategory: ProductUsageCategory.trousers, + season: ProductSeason.winter, + widthCm: 150, + composition: '۶۰٪ پشم، ۴۰٪ پلی‌استر', + weightGrams: 300, + colorsJson: ['#111827', '#f8fafc'], + tagsJson: ['شلواری', 'زمستانه'], + subtitle: 'گرم زمستانه', + rollMeters: 50, + isNew: false, + }, + { + id: 'prod-fam-401-1', + companyId: 'company-fam-baft', + catalogId: 'cat-fam-401', + name: 'پارچه مخمل', + code: '401-MV', + color: 'سرمه‌ای', + minMeterage: 40, + orderMultiplier: 10, + basePrice: 250000, + stock: 6000, + isActive: true, + usageCategory: ProductUsageCategory.suit, + season: ProductSeason.winter, + widthCm: 140, + composition: 'مخمل پلی‌استر', + weightGrams: 320, + colorsJson: ['#1e3a5f', '#7c2d12', '#365314'], + tagsJson: ['کت‌شلواری', 'زمستانه'], + subtitle: 'مخمل لوکس', + rollMeters: 45, + isNew: true, + }, + { + id: 'prod-fam-401-2', + companyId: 'company-fam-baft', + catalogId: 'cat-fam-401', + name: 'پارچه کجراه', + code: '401-KJ', + color: 'کرم', + minMeterage: 40, + orderMultiplier: 10, + basePrice: null, + stock: 4000, + isActive: true, + usageCategory: ProductUsageCategory.trousers, + season: ProductSeason.all_season, + widthCm: 150, + composition: 'کجراه پنبه‌ای', + weightGrams: 240, + colorsJson: ['#d6c3a8', '#8b7355'], + tagsJson: ['شلواری', 'چهارفصل'], + subtitle: 'روزمره', + rollMeters: 50, + isNew: false, + }, + ], + }); + + await prisma.customer.create({ + data: { + id: 'agent-1', + firstName: 'علی', + lastName: 'رضایی', + phone: '09121112233', + nationalId: '0011111111', + customerType: CustomerType.wholesale, + appRole: AppUserRole.agent, + agentCode: 'W-1247', + levelLabel: 'طلایی', + nextLevelLabel: 'VIP', + pointsToNext: 550_000_000, + levelProgress: 72, + companyId: 'company-par', + isActive: true, + avatarLetter: 'ع', + avatarColor: '#0B3D33', + createdAt: new Date('2024-01-10T10:00:00'), + }, + }); + + await prisma.customer.createMany({ + data: [ + { + id: 'cust-1', + firstName: 'سارا', + lastName: 'محمدی', + phone: '09121234567', + nationalId: '0012345678', + customerType: CustomerType.wholesale, + companyId: 'company-par', + businessName: 'بوتیک رز', + assignedAgentId: 'agent-1', + levelLabel: 'نقره‌ای', + avatarLetter: 'پ', + avatarColor: '#f9a8d4', + isActive: true, + createdAt: new Date('2024-03-12T10:00:00'), + }, + { + id: 'cust-2', + firstName: 'نرگس', + lastName: 'احمدی', + phone: '09135551234', + nationalId: '1030567890', + customerType: CustomerType.wholesale, + companyId: 'company-fam-baft', + businessName: 'مزون الوان', + assignedAgentId: 'agent-1', + levelLabel: 'برنزی', + avatarLetter: 'م', + avatarColor: '#c4b5fd', + isActive: true, + createdAt: new Date('2024-05-20T14:30:00'), + }, + { + id: 'cust-3', + firstName: 'فروشگاه', + lastName: 'پارچه مهر', + phone: '09128877665', + nationalId: '0012345679', + customerType: CustomerType.retail, + companyId: 'company-par', + businessName: 'پارچه مهر', + assignedAgentId: 'agent-1', + levelLabel: 'برنزی', + avatarLetter: 'م', + avatarColor: '#93c5fd', + isActive: true, + createdAt: new Date('2024-07-08T09:15:00'), + }, + { + id: 'cust-4', + firstName: 'پوشاک', + lastName: 'سپهر', + phone: '09369876543', + nationalId: '1050987654', + customerType: CustomerType.wholesale, + companyId: 'company-par', + businessName: 'پوشاک سپهر', + assignedAgentId: 'agent-1', + levelLabel: 'برنزی', + avatarLetter: 'س', + avatarColor: '#fcd34d', + isActive: false, + createdAt: new Date('2024-08-01T11:00:00'), + }, + { + id: 'cust-5', + firstName: 'بافت', + lastName: 'آسان', + phone: '09121110000', + nationalId: '0065432109', + customerType: CustomerType.wholesale, + companyId: 'company-fam-baft', + businessName: 'بافت آسان', + assignedAgentId: 'agent-1', + levelLabel: 'نقره‌ای', + avatarLetter: 'ب', + avatarColor: '#6ee7b7', + isActive: true, + createdAt: new Date('2024-06-01T10:00:00'), + }, + { + id: 'cust-6', + firstName: 'مینا', + lastName: 'کریمی', + phone: '09129990001', + nationalId: '0077007700', + customerType: CustomerType.retail, + companyId: 'company-par', + businessName: 'مزون مینا', + assignedAgentId: 'agent-1', + levelLabel: 'برنزی', + avatarLetter: 'ن', + avatarColor: '#fda4af', + isActive: true, + createdAt: new Date('2024-09-01T10:00:00'), + }, + ], + }); + + const order1 = await prisma.order.create({ + data: { + id: 'ord-1', + orderNumber: 'ORD-1403-1256', + userId: 'cust-1', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.check, + checkMode: CheckRegistrationMode.full, + status: OrderStatus.awaiting_review, + totalMeterage: 4250, + totalAmount: 2_450_000_000, + createdAt: new Date('2024-10-25T10:30:00'), + items: { + create: [ + { + id: 'oi-1', + productId: 'prod-377-blue', + catalogCode: '377', + productName: 'پارچه آبی', + productCode: '377-BL', + color: 'آبی', + requestedMeterage: 2000, + confirmedMeterage: 2000, + }, + { + id: 'oi-2', + productId: 'prod-377-black', + catalogCode: '377', + productName: 'پارچه مشکی', + productCode: '377-BK', + color: 'مشکی', + requestedMeterage: 1500, + confirmedMeterage: 1000, + }, + { + id: 'oi-3', + productId: 'prod-378-blue-white', + catalogCode: '378', + productName: 'آبی پشت سفید', + productCode: '378-BLW', + color: 'آبی پشت سفید', + requestedMeterage: 750, + confirmedMeterage: 750, + }, + ], + }, + checks: { + create: [ + { + id: 'chk-1', + row: 1, + amount: 1_000_000_000, + bankName: 'ملت', + sayadId: '1234567890123456', + dueDate: new Date('2024-10-21'), + status: CheckStatus.pending_review, + mode: CheckRegistrationMode.full, + }, + { + id: 'chk-2', + row: 2, + amount: 800_000_000, + bankName: 'ملی', + sayadId: '2345678901234567', + dueDate: new Date('2024-10-26'), + status: CheckStatus.pending_review, + mode: CheckRegistrationMode.full, + }, + { + id: 'chk-3', + row: 3, + amount: 650_000_000, + bankName: 'ملت', + sayadId: '3456789012345678', + dueDate: new Date('2024-10-31'), + status: CheckStatus.pending_review, + mode: CheckRegistrationMode.full, + }, + ], + }, + statusHistory: { + create: [ + { fromStatus: null, toStatus: OrderStatus.submitted, changedBy: 'customer', changedAt: new Date('2024-10-25T10:30:00') }, + { fromStatus: OrderStatus.submitted, toStatus: OrderStatus.awaiting_review, changedBy: 'system', changedAt: new Date('2024-10-25T10:30:01') }, + ], + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-2', + orderNumber: 'ORD-1403-1255', + userId: 'cust-2', + companyId: 'company-fam-baft', + companyName: 'فام‌بافت', + settlementType: SettlementType.check, + checkMode: CheckRegistrationMode.brief, + status: OrderStatus.awaiting_check_review, + totalMeterage: 3200, + totalAmount: 1_850_000_000, + createdAt: new Date('2024-10-24T14:15:00'), + items: { + create: [ + { + productId: 'prod-fam-401-1', + catalogCode: '401', + productName: 'پارچه مخمل', + productCode: '401-MV', + color: 'سرمه‌ای', + requestedMeterage: 3200, + confirmedMeterage: 3200, + unitPrice: 250000, + lineTotal: 800_000_000, + }, + ], + }, + cashReceipt: { + create: { + id: 'cp-1', + amount: 1_850_000_000, + paidAmount: 1_850_000_000, + transferDate: new Date('2024-10-25T08:30:00'), + trackingNumber: '9876543210', + expiresAt: new Date('2024-10-25T17:00:00'), + status: CashPaymentStatus.pending_review, + submittedAt: new Date('2024-10-25T08:30:00'), + }, + }, + statusHistory: { + create: [ + { fromStatus: null, toStatus: OrderStatus.submitted, changedBy: 'customer' }, + { fromStatus: OrderStatus.submitted, toStatus: OrderStatus.awaiting_check_review, changedBy: 'admin' }, + ], + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-3', + orderNumber: 'ORD-1403-1254', + userId: 'cust-1', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.cash, + status: OrderStatus.final_approved, + totalMeterage: 1800, + totalAmount: 980_000_000, + finalAmount: 980_000_000, + createdAt: new Date('2024-10-23T09:00:00'), + cashReceipt: { + create: { + id: 'cp-2', + amount: 980_000_000, + paidAmount: 980_000_000, + transferDate: new Date('2024-10-24T15:20:00'), + trackingNumber: '1234567890', + destinationAccountId: 'ba-par-1', + expiresAt: new Date('2024-10-24T17:00:00'), + status: CashPaymentStatus.approved, + submittedAt: new Date('2024-10-24T15:20:00'), + }, + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-4', + orderNumber: 'ORD-1403-1253', + userId: 'cust-5', + companyId: 'company-fam-baft', + companyName: 'فام‌بافت', + settlementType: SettlementType.check, + status: OrderStatus.check_needs_fix, + totalMeterage: 5100, + totalAmount: 3_200_000_000, + createdAt: new Date('2024-10-22T16:45:00'), + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-5', + orderNumber: 'ORD-1403-1252', + userId: 'cust-1', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.cash, + status: OrderStatus.delivered, + totalMeterage: 1200, + totalAmount: 750_000_000, + finalAmount: 750_000_000, + createdAt: new Date('2024-10-21T11:20:00'), + shipment: { + create: { + id: 'shp-delivered-5', + carrier: 'باربری تهران', + trackingNumber: 'BR-1403-88001', + meterage: 1200, + status: ShipmentStatus.delivered, + shippedAt: new Date('2024-10-19T09:00:00'), + deliveredAt: new Date('2024-10-21T11:00:00'), + }, + }, + cashReceipt: { + create: { + id: 'cp-3', + amount: 750_000_000, + paidAmount: 750_000_000, + transferDate: new Date('2024-10-18T11:00:00'), + trackingNumber: '5555123456', + destinationAccountId: 'ba-par-2', + expiresAt: new Date('2024-10-18T17:00:00'), + status: CashPaymentStatus.approved, + submittedAt: new Date('2024-10-18T11:00:00'), + }, + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-6', + orderNumber: 'ORD-1403-1251', + userId: 'cust-1', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.check, + status: OrderStatus.shipped, + totalMeterage: 2900, + totalAmount: 1_600_000_000, + createdAt: new Date('2024-10-20T08:30:00'), + shipment: { + create: { + id: 'shp-1', + carrier: 'باربری تهران-اصفهان', + trackingNumber: 'BR-1403-88901', + meterage: 2900, + status: ShipmentStatus.shipped, + shippedAt: new Date('2024-10-24T09:00:00'), + }, + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-7', + orderNumber: 'ORD-1403-1250', + userId: 'cust-5', + companyId: 'company-fam-baft', + companyName: 'فام‌بافت', + settlementType: SettlementType.check, + status: OrderStatus.awaiting_check_review, + totalMeterage: 3800, + totalAmount: 2_100_000_000, + createdAt: new Date('2024-10-19T13:00:00'), + shipment: { + create: { + id: 'shp-2', + carrier: 'باربری سریع', + trackingNumber: 'BR-1403-88902', + meterage: 3800, + status: ShipmentStatus.ready, + }, + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-8', + orderNumber: 'ORD-1403-1208', + userId: 'cust-3', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.cash, + status: OrderStatus.preparing, + totalMeterage: 950, + totalAmount: 450_000_000, + createdAt: new Date('2024-10-18T10:00:00'), + shipment: { + create: { + id: 'shp-3', + carrier: 'پیک داخلی', + trackingNumber: 'PK-1403-0045', + meterage: 950, + status: ShipmentStatus.preparing, + }, + }, + cashReceipt: { + create: { + id: 'cp-4', + amount: 450_000_000, + paidAmount: null, + transferDate: null, + trackingNumber: null, + expiresAt: new Date('2024-10-22T17:00:00'), + status: CashPaymentStatus.expired, + }, + }, + }, + }); + + await prisma.order.create({ + data: { + id: 'ord-9', + orderNumber: 'ORD-1403-1199', + userId: 'cust-1', + companyId: 'company-par', + companyName: 'شرکت پَر', + settlementType: SettlementType.check, + status: OrderStatus.delivered, + totalMeterage: 4100, + totalAmount: 2_200_000_000, + createdAt: new Date('2024-10-15T10:00:00'), + shipment: { + create: { + id: 'shp-4', + carrier: 'باربری تهران-مشهد', + trackingNumber: 'BR-1403-88750', + meterage: 4100, + status: ShipmentStatus.delivered, + shippedAt: new Date('2024-10-18T10:00:00'), + deliveredAt: new Date('2024-10-20T16:30:00'), + }, + }, + }, + }); + + await prisma.systemSettings.create({ + data: { + id: 'default', + maxMeteragePerInvoice: 5000, + cashInvoiceValidityHours: 8, + defaultSettlementCheck: true, + defaultSettlementCash: true, + smsOrderRegistered: true, + smsPaymentReminder: true, + smsCheckReview: true, + smsShippingUpdate: true, + supportPhone: '02191000000', + supportEmail: 'support@fabrictrade.ir', + }, + }); + + await prisma.meetingInteraction.createMany({ + data: [ + { + id: 'meet-1', + agentId: 'agent-1', + customerId: 'cust-1', + type: InteractionType.in_person, + title: 'بازدید از بوتیک رز', + description: 'نیاز به پارچه‌های طوسی و سرمه‌ای اعلام شد.', + subject: 'بازدید حضوری', + content: 'نیاز به پارچه‌های طوسی، سرمه‌ای و ذغالی با عرض مناسب.', + date: '1403/03/20', + time: '11:30', + }, + { + id: 'meet-2', + agentId: 'agent-1', + customerId: 'cust-2', + type: InteractionType.call, + title: 'پیگیری موجودی', + description: 'تماس برای اعلام موجودی فاستونی.', + date: '1403/03/18', + time: '10:00', + }, + ], + }); + + await prisma.reminder.createMany({ + data: [ + { + id: 'rem-1', + ownerId: 'agent-1', + customerId: 'cust-1', + title: 'پیگیری موجودی پارچه فاستونی طوسی', + description: 'اعلام موجودی و ارسال نمونه', + note: 'تماس برای اعلام موجودی و ارسال نمونه', + date: '1403/03/23', + time: '10:00', + interactionType: InteractionType.call, + completed: false, + }, + { + id: 'rem-2', + ownerId: 'agent-1', + customerId: 'cust-2', + title: 'ارسال کالیته جدید', + description: 'ارسال کالیته زمستانه برای مزون الوان', + date: '1403/03/24', + time: '14:00', + interactionType: InteractionType.in_person, + completed: false, + }, + { + id: 'rem-3', + ownerId: 'agent-1', + customerId: 'cust-3', + title: 'پیگیری سفارش قبلی', + description: 'بررسی وضعیت سفارش و تسویه', + date: '1403/03/10', + time: '09:00', + interactionType: InteractionType.note, + completed: true, + completedAt: new Date('2024-10-20T09:30:00'), + }, + ], + }); + + void order1; + console.log('Seed completed successfully.'); + console.log('Admin login: 09128081392 / 123456'); + console.log('Agent OTP login: 09121112233'); + console.log('Customer OTP login: 09121234567'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/build-and-export-image.sh b/scripts/build-and-export-image.sh new file mode 100644 index 0000000..1c26b20 --- /dev/null +++ b/scripts/build-and-export-image.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +IMAGE_NAME="${IMAGE_NAME:-fabric-trade-api}" +IMAGE_TAG="${IMAGE_TAG:-latest}" +FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +OUT_DIR="${OUT_DIR:-./dist-docker}" +OUT_TAR="${OUT_DIR}/${IMAGE_NAME}-${IMAGE_TAG}.tar" +NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}" + +mkdir -p "$OUT_DIR" + +echo "==> Building image locally: $FULL_IMAGE" +echo "==> npm registry: $NPM_REGISTRY" + +docker build \ + --build-arg "NPM_REGISTRY=${NPM_REGISTRY}" \ + -t "$FULL_IMAGE" \ + -f Dockerfile \ + . + +echo "==> Saving image to $OUT_TAR ..." +docker save "$FULL_IMAGE" -o "$OUT_TAR" + +# فشرده‌سازی برای آپلود سریع‌تر +echo "==> Compressing..." +gzip -f -k "$OUT_TAR" +echo "" +echo "آماده:" +echo " $OUT_TAR" +echo " ${OUT_TAR}.gz" +ls -lh "$OUT_TAR" "${OUT_TAR}.gz" +echo "" +echo "آپلود روی سرور:" +echo " scp ${OUT_TAR}.gz root@SERVER:/opt/parche/server/" +echo "" +echo "روی سرور:" +echo " gunzip -f fabric-trade-api-latest.tar.gz" +echo " docker load -i fabric-trade-api-latest.tar" +echo " RUN_SEED=true docker compose -f docker-compose.image.yml up -d" diff --git a/scripts/build-local-dist.sh b/scripts/build-local-dist.sh new file mode 100644 index 0000000..10c11f4 --- /dev/null +++ b/scripts/build-local-dist.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +MIRROR="${NPM_REGISTRY:-https://registry.npmmirror.com}" + +echo "==> بیلد لوکال بدون Docker" +echo "==> registry: $MIRROR" +echo "==> node: $(node -v)" + +npm config set registry "$MIRROR" + +# Prisma postinstall باید اجازه اجرا داشته باشد +npm config set ignore-scripts false || true + +if [ ! -d node_modules ]; then + echo "==> npm ci..." + npm ci --no-audit --no-fund +fi + +# اگر اسکریپت‌های prisma بلاک شده باشند +npx --yes prisma generate + +echo "==> nest/tsc build (با پاک کردن کش)..." +rm -rf dist tsconfig.build.tsbuildinfo +npx nest build + +if [ ! -f dist/main.js ]; then + echo "==> nest build خروجی نداد؛ fallback به tsc..." + rm -rf dist tsconfig.build.tsbuildinfo + npx tsc -p tsconfig.build.json +fi + +if [ ! -f dist/main.js ]; then + echo "ERROR: dist/main.js ساخته نشد" + ls -la dist || true + exit 1 +fi + +echo "==> OK: dist/main.js ($(wc -c < dist/main.js) bytes)" diff --git a/scripts/pack-bundle.sh b/scripts/pack-bundle.sh new file mode 100644 index 0000000..d949f75 --- /dev/null +++ b/scripts/pack-bundle.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +MIRROR="${NPM_REGISTRY:-https://registry.npmmirror.com}" +OUT="deploy-bundle.tar.gz" +PACK=".deploy-pack" + +echo "==> بیلد + بسته‌بندی کامل (dist + node_modules)" +echo "==> node: $(node -v)" + +npm config set registry "$MIRROR" +npm ci --no-audit --no-fund +DATABASE_URL="${DATABASE_URL:-postgresql://fabric:fabric123@localhost:5432/fabric_trade}" npx prisma generate +rm -rf dist tsconfig.build.tsbuildinfo +npx nest build +test -f dist/main.js + +rm -rf "$PACK" /tmp/fabric-prod-pack +mkdir -p "$PACK" /tmp/fabric-prod-pack + +cp package.json package-lock.json /tmp/fabric-prod-pack/ +cp -a prisma /tmp/fabric-prod-pack/ +cp prisma.config.ts /tmp/fabric-prod-pack/ +( + cd /tmp/fabric-prod-pack + sed -i "s|https://registry.npmjs.org|${MIRROR}|g" package-lock.json || true + npm ci --omit=dev --no-audit --no-fund --registry="$MIRROR" + npm install prisma ts-node typescript --no-save --no-audit --no-fund --registry="$MIRROR" + DATABASE_URL="${DATABASE_URL:-postgresql://fabric:fabric123@localhost:5432/fabric_trade}" npx prisma generate +) + +cp package.json package-lock.json "$PACK/" +cp -a prisma "$PACK/" +cp prisma.config.ts docker-entrypoint.sh "$PACK/" +cp -a dist "$PACK/" +cp -a /tmp/fabric-prod-pack/node_modules "$PACK/" +cp Dockerfile.bundle "$PACK/Dockerfile" +cp docker-compose.bundle.yml "$PACK/docker-compose.yml" +cp .env.example "$PACK/" + +cat > "$PACK/.dockerignore" <<'EOF' +.git +.env +*.md +EOF + +tar -czf "$OUT" -C "$PACK" . +ls -lh "$OUT" +echo "" +echo "فایل آماده: $(pwd)/$OUT" +echo "آپلود:" +echo " scp $(pwd)/$OUT root@SERVER:/opt/parche/" +echo "روی سرور:" +echo " mkdir -p /opt/parche/app && tar -xzf /opt/parche/deploy-bundle.tar.gz -C /opt/parche/app" +echo " cd /opt/parche/app && cp -n .env.example .env" +echo " RUN_SEED=true docker compose up -d --build" diff --git a/scripts/pack-prisma-client.sh b/scripts/pack-prisma-client.sh new file mode 100644 index 0000000..1016d7a --- /dev/null +++ b/scripts/pack-prisma-client.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# کلاینت Prisma را روی سیستم دولوپر می‌سازد و بسته‌بندی می‌کند. +# سرور نباید prisma generate بزند؛ روی هاست پروडاکشن دقایق طول می‌کشد. +set -euo pipefail +cd "$(dirname "$0")/.." + +OUT="prisma-client.tar.gz" +SRC="node_modules/.prisma/client" + +if [ ! -d node_modules/prisma ]; then + echo "ERROR: node_modules نیست. اول npm ci بزن." >&2 + exit 1 +fi + +# خروجی قبلی پاک شود تا engineهای بی‌استفاده در بسته نمانند +rm -rf "$SRC" + +DATABASE_URL="${DATABASE_URL:-postgresql://build:build@localhost:5432/build?schema=public}" \ + CHECKPOINT_DISABLE=1 ./node_modules/.bin/prisma generate + +test -f "$SRC/libquery_engine-debian-openssl-3.0.x.so.node" \ + || { echo "ERROR: engine مربوط به debian-openssl-3.0.x ساخته نشد." >&2; exit 1; } + +rm -f "$OUT" +tar -czf "$OUT" -C node_modules/.prisma client +ls -lh "$OUT" diff --git a/scripts/reset-admin.mjs b/scripts/reset-admin.mjs new file mode 100644 index 0000000..52de742 --- /dev/null +++ b/scripts/reset-admin.mjs @@ -0,0 +1,43 @@ +/** + * ادمین‌های قبلی را پاک می‌کند و فقط یک ادمین می‌سازد. + * داخل کانتینر API: + * node scripts/reset-admin.mjs + * یا با DATABASE_URL محلی: + * node scripts/reset-admin.mjs + */ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +const PHONE = process.env.ADMIN_PHONE || '09128081392'; +const PASSWORD = process.env.ADMIN_PASSWORD || '123456'; +const FULL_NAME = process.env.ADMIN_NAME || 'مدیر سیستم'; + +async function main() { + const prisma = new PrismaClient(); + try { + const passwordHash = await bcrypt.hash(PASSWORD, 10); + await prisma.$transaction([ + prisma.adminUser.deleteMany({}), + prisma.adminUser.create({ + data: { + id: 'usr-1', + fullName: FULL_NAME, + phone: PHONE, + email: 'admin@parche.ir', + passwordHash, + role: 'admin', + isActive: true, + }, + }), + prisma.internalMessage.deleteMany({}), + ]); + console.log(`Admin reset OK: ${PHONE} / ${PASSWORD}`); + } finally { + await prisma.$disconnect(); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/server-up.sh b/scripts/server-up.sh new file mode 100644 index 0000000..7e469d7 --- /dev/null +++ b/scripts/server-up.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# روی سرور: یک دستور +set -euo pipefail +cd "$(dirname "$0")/.." +git pull --no-rebase +docker compose up -d --build +docker compose ps +echo "health: curl -s http://127.0.0.1:3000/api/health" diff --git a/scripts/smoke-order-flow.mjs b/scripts/smoke-order-flow.mjs new file mode 100644 index 0000000..0862373 --- /dev/null +++ b/scripts/smoke-order-flow.mjs @@ -0,0 +1,530 @@ +/** + * تست دود گردش کامل سفارش مطابق روال کارفرما: + * مشتری بدون دیدن قیمت سفارش می‌دهد، ادمین قیمت می‌گذارد و فاکتور می‌فرستد، + * مشتری روش تسویه را انتخاب می‌کند و مسیر نقدی/چکی تا تایید نهایی پیش می‌رود. + * اجرا: node scripts/smoke-order-flow.mjs + */ +const BASE = process.env.SMOKE_BASE_URL || 'http://127.0.0.1:3999/api'; +const CUSTOMER_PHONE = '09121234567'; +const ADMIN_PHONE = '09128081392'; +const ADMIN_PASSWORD = '123456'; + +let passed = 0; +let failed = 0; + +function check(name, condition, detail) { + if (condition) { + passed++; + console.log(` PASS ${name}`); + } else { + failed++; + console.log(` FAIL ${name}${detail ? ` → ${detail}` : ''}`); + } +} + +async function api(path, { method = 'GET', body, token } = {}) { + const res = await fetch(`${BASE}${path}`, { + method, + headers: { + Accept: 'application/json', + ...(body ? { 'Content-Type': 'application/json' } : {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + let data = null; + try { + data = text ? JSON.parse(text) : null; + } catch { + data = text; + } + return { status: res.status, data }; +} + +function isoInDays(days) { + return new Date(Date.now() + days * 86400000).toISOString().slice(0, 10); +} + +/** تصویر base64 بزرگ‌تر از سقف پیش‌فرض ۱۰۰KB اکسپرس */ +function bigBase64(kb) { + return `data:image/jpeg;base64,${'A'.repeat(kb * 1024)}`; +} + +function checkPayload(amount, days, extra = {}) { + return { + amount, + dueDate: isoInDays(days), + sayadId: '1234567890123456', + checkNumber: '123456', + ownerNationalId: '0012345678', + ownerFirstName: 'علی', + ownerLastName: 'رضایی', + bankName: 'ملی', + branchName: 'مرکزی', + mode: 'full', + ...extra, + }; +} + +async function main() { + console.log(`\n== Smoke test @ ${BASE} ==\n`); + + console.log('[1] احراز هویت'); + const otp = await api('/auth/otp/request', { + method: 'POST', + body: { phone: CUSTOMER_PHONE }, + }); + check('درخواست OTP', otp.status === 201 || otp.status === 200, `status=${otp.status}`); + const code = otp.data?.debugCode ?? '1234'; + check('کد OTP چهار رقمی است', /^\d{4}$/.test(String(code)), `code=${code}`); + + const badVerify = await api('/auth/otp/verify', { + method: 'POST', + body: { phone: CUSTOMER_PHONE, code: String(code) === '0000' ? '1111' : '0000' }, + }); + check('کد اشتباه رد می‌شود', badVerify.status === 401, `status=${badVerify.status}`); + + const verify = await api('/auth/otp/verify', { + method: 'POST', + body: { phone: CUSTOMER_PHONE, code: String(code) }, + }); + check('ورود مشتری', !!verify.data?.accessToken, JSON.stringify(verify.data).slice(0, 120)); + const userToken = verify.data?.accessToken; + + const adminLogin = await api('/admin/auth/login', { + method: 'POST', + body: { phone: ADMIN_PHONE, password: ADMIN_PASSWORD }, + }); + check('ورود ادمین', !!adminLogin.data?.accessToken, JSON.stringify(adminLogin.data).slice(0, 120)); + const adminToken = adminLogin.data?.accessToken; + + console.log('\n[2] جداسازی دسترسی'); + const crossAccess = await api('/admin/orders', { token: userToken }); + check('توکن مشتری به مسیر ادمین دسترسی ندارد', crossAccess.status === 403, `status=${crossAccess.status}`); + const custWithAdmin = await api('/orders', { token: adminToken }); + check('توکن ادمین به مسیر مشتری دسترسی ندارد', custWithAdmin.status === 403, `status=${custWithAdmin.status}`); + const noToken = await api('/orders'); + check('بدون توکن ۴۰۱', noToken.status === 401, `status=${noToken.status}`); + + console.log('\n[3] سبد خرید بدون قیمت'); + const companies = await api('/companies/public'); + const company = companies.data?.[0]; + check('لیست شرکت‌ها', !!company, JSON.stringify(companies.data).slice(0, 120)); + check( + 'سقف متراژ موثر برگردانده می‌شود', + typeof company?.maxMeteragePerInvoice === 'number' && company.maxMeteragePerInvoice > 0, + `max=${company?.maxMeteragePerInvoice}`, + ); + + const products = await api(`/products?companyId=${company.id}`, { token: userToken }); + const product = (Array.isArray(products.data) ? products.data : products.data?.items)?.find( + (p) => p.companyId === company.id, + ); + check('لیست محصولات', !!product, JSON.stringify(products.data).slice(0, 160)); + + const meterage = Math.max(product.minMeterage ?? 1, product.orderMultiplier ?? 1); + await api('/carts', { method: 'DELETE', token: userToken }); + const addItem = await api('/carts/items', { + method: 'POST', + token: userToken, + body: { productId: product.id, meterage }, + }); + check('افزودن به سبد', addItem.status < 300, `status=${addItem.status} ${JSON.stringify(addItem.data).slice(0, 120)}`); + const cartItem = addItem.data?.items?.[0]; + check('سبد قیمت واحد ندارد', cartItem?.unitPrice === undefined, `unitPrice=${cartItem?.unitPrice}`); + + console.log('\n[4] ثبت سفارش بدون روش تسویه و بدون قیمت'); + const cashOrder = await api('/orders', { + method: 'POST', + token: userToken, + body: { companyId: company.id, items: [{ productId: product.id, meterage }] }, + }); + check('ثبت سفارش', cashOrder.status < 300, `status=${cashOrder.status} ${JSON.stringify(cashOrder.data).slice(0, 200)}`); + const cashOrderId = cashOrder.data?.id; + check('وضعیت اولیه در انتظار بررسی', cashOrder.data?.status === 'awaiting_review', `status=${cashOrder.data?.status}`); + check('روش تسویه خالی است', cashOrder.data?.settlementType == null, `settlement=${cashOrder.data?.settlementType}`); + check('مبلغ برای مشتری پنهان است', cashOrder.data?.finalAmount == null && cashOrder.data?.totalAmount == null); + check('پرچم pricesVisible خاموش است', cashOrder.data?.pricesVisible === false, `flag=${cashOrder.data?.pricesVisible}`); + check( + 'قیمت واحد آیتم‌ها پنهان است', + (cashOrder.data?.items ?? []).every((i) => i.unitPrice == null), + ); + + console.log('\n[5] اعتبارسنجی ورودی'); + const extraField = await api('/orders', { + method: 'POST', + token: userToken, + body: { companyId: company.id, items: [{ productId: product.id, meterage }], bogusField: 'x' }, + }); + check('فیلد ناشناخته ۴۰۰ می‌دهد', extraField.status === 400, `status=${extraField.status}`); + + const earlySettlement = await api(`/orders/${cashOrderId}/settlement`, { + method: 'POST', + token: userToken, + body: { settlementType: 'cash' }, + }); + check('انتخاب تسویه قبل از فاکتور رد می‌شود', earlySettlement.status === 400, `status=${earlySettlement.status}`); + + const earlyReceipt = await api(`/orders/${cashOrderId}/cash-receipt`, { + method: 'POST', + token: userToken, + body: { + amount: 1000, + transferDate: isoInDays(0), + transferTime: '12:00', + trackingNumber: '1', + destinationAccountId: 'x', + }, + }); + check('رسید قبل از فاکتور رد می‌شود', earlyReceipt.status === 400, `status=${earlyReceipt.status}`); + + console.log('\n[6] سطل‌های وضعیت ادمین'); + const adminOrders = await api('/admin/orders', { token: adminToken }); + const adminIds = (adminOrders.data ?? []).map((o) => o.id); + check('سفارش برای ادمین دیده می‌شود', adminIds.includes(cashOrderId)); + const allowedTabs = new Set([ + 'pending_review', + 'awaiting_customer', + 'pending_check', + 'pending_payment', + 'needs_correction', + 'check_correction', + 'final_approval', + 'shipped', + 'delivered', + 'cancelled', + 'rejected', + 'expired', + ]); + const unknown = [...new Set((adminOrders.data ?? []).map((o) => o.adminStatus))].filter( + (s) => !allowedTabs.has(s), + ); + check('همه وضعیت‌ها به تب معتبر نگاشت شده‌اند', unknown.length === 0, `unknown=${unknown.join(',')}`); + + const filtered = await api('/admin/orders?status=pending_review', { token: adminToken }); + check( + 'فیلتر «در انتظار بررسی» سفارش را برمی‌گرداند', + (filtered.data ?? []).some((o) => o.id === cashOrderId), + `count=${(filtered.data ?? []).length}`, + ); + + console.log('\n[7] قیمت‌گذاری ادمین'); + const detail = await api(`/admin/orders/${cashOrderId}`, { token: adminToken }); + const items = detail.data?.items ?? []; + check('سفارش آیتم دارد', items.length > 0); + check( + 'ادمین گذرهای مجاز را می‌بیند', + Array.isArray(detail.data?.allowedTransitions) && detail.data.allowedTransitions.includes('pricing'), + `transitions=${JSON.stringify(detail.data?.allowedTransitions)}`, + ); + + const earlyInvoice = await api(`/admin/orders/${cashOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'awaiting_settlement' }, + }); + check('ارسال فاکتور بدون قیمت‌گذاری رد می‌شود', earlyInvoice.status === 400, `status=${earlyInvoice.status}`); + + const pricing = await api(`/admin/orders/${cashOrderId}/pricing`, { + method: 'PATCH', + token: adminToken, + body: { + items: [{ id: items[0].id, confirmedMeterage: meterage, unitPrice: 100000, unitDiscount: 0 }], + shippingCost: 50000, + }, + }); + const expectedTotal = 100000 * meterage; + const expectedFinal = expectedTotal + 50000; + check('قیمت‌گذاری موفق', pricing.status < 300, `status=${pricing.status} ${JSON.stringify(pricing.data).slice(0, 150)}`); + check('جمع کل درست است', Math.abs((pricing.data?.totalAmount ?? 0) - expectedTotal) < 0.01, `total=${pricing.data?.totalAmount}`); + check('مبلغ نهایی = جمع + حمل', Math.abs((pricing.data?.finalAmount ?? 0) - expectedFinal) < 0.01, `final=${pricing.data?.finalAmount}`); + + const stillHidden = await api(`/orders/${cashOrderId}`, { token: userToken }); + check('قیمت تا ارسال فاکتور برای مشتری پنهان است', stillHidden.data?.finalAmount == null, `final=${stillHidden.data?.finalAmount}`); + + const badDiscount = await api(`/admin/orders/${cashOrderId}/pricing`, { + method: 'PATCH', + token: adminToken, + body: { items: [{ id: items[0].id, unitPrice: 1000, unitDiscount: 5000 }] }, + }); + check('تخفیف بیشتر از قیمت رد می‌شود', badDiscount.status === 400, `status=${badDiscount.status}`); + + console.log('\n[8] ارسال فاکتور برای مشتری'); + await api(`/admin/orders/${cashOrderId}/pricing`, { + method: 'PATCH', + token: adminToken, + body: { + items: [{ id: items[0].id, confirmedMeterage: meterage, unitPrice: 100000, unitDiscount: 0 }], + shippingCost: 50000, + }, + }); + const invoice = await api(`/admin/orders/${cashOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'awaiting_settlement', note: 'فاکتور نهایی شد' }, + }); + check('ارسال فاکتور', invoice.status < 300, `status=${invoice.status} ${JSON.stringify(invoice.data).slice(0, 150)}`); + + const priced = await api(`/orders/${cashOrderId}`, { token: userToken }); + check('مشتری مبلغ فاکتور را می‌بیند', Math.abs((priced.data?.finalAmount ?? 0) - expectedFinal) < 0.01, `final=${priced.data?.finalAmount}`); + check('pricesVisible روشن شد', priced.data?.pricesVisible !== false); + + const illegal = await api(`/admin/orders/${cashOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'delivered' }, + }); + check('گذر غیرمجاز بدون force رد می‌شود', illegal.status === 400, `status=${illegal.status}`); + const forceNoReason = await api(`/admin/orders/${cashOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'delivered', force: true }, + }); + check('force بدون دلیل رد می‌شود', forceNoReason.status === 400, `status=${forceNoReason.status}`); + + console.log('\n[9] اعلان مشتری'); + const notifs = await api('/notifications', { token: userToken }); + const invoiceNotif = (notifs.data ?? []).find((n) => n.event === 'invoice_priced'); + check('اعلان صدور فاکتور ساخته شد', !!invoiceNotif, JSON.stringify(notifs.data)?.slice(0, 200)); + check('اعلان به سفارش لینک دارد', invoiceNotif?.link === `/orders/${cashOrderId}`, `link=${invoiceNotif?.link}`); + const unread = await api('/notifications/unread-count', { token: userToken }); + check('شمارنده خوانده‌نشده مثبت است', (unread.data?.count ?? 0) > 0, `count=${unread.data?.count}`); + if (invoiceNotif) { + const marked = await api(`/notifications/${invoiceNotif.id}/read`, { method: 'PATCH', token: userToken }); + check('علامت خوانده‌شده', marked.status < 300 && marked.data?.count < (unread.data?.count ?? 0), JSON.stringify(marked.data)); + } + const readAll = await api('/notifications/read-all', { method: 'PATCH', token: userToken }); + check('خواندن همه', readAll.status < 300 && (readAll.data?.count ?? -1) === 0, JSON.stringify(readAll.data)); + + console.log('\n[10] مسیر نقدی'); + const chooseCash = await api(`/orders/${cashOrderId}/settlement`, { + method: 'POST', + token: userToken, + body: { settlementType: 'cash' }, + }); + check('انتخاب تسویه نقدی', chooseCash.status < 300, `status=${chooseCash.status} ${JSON.stringify(chooseCash.data).slice(0, 150)}`); + check('وضعیت در انتظار پرداخت', chooseCash.data?.status === 'awaiting_cash_payment', `status=${chooseCash.data?.status}`); + const expiry = chooseCash.data?.cashInvoiceExpiresAt; + check('انقضای فاکتور در آینده است', !!expiry && new Date(expiry) > new Date(), `expiry=${expiry}`); + + const accounts = await api(`/companies/${company.id}/bank-accounts`, { token: userToken }); + const account = (accounts.data ?? [])[0]; + check('حساب بانکی موجود', !!account, JSON.stringify(accounts.data).slice(0, 120)); + + const receipt = await api(`/orders/${cashOrderId}/cash-receipt`, { + method: 'POST', + token: userToken, + body: { + amount: expectedFinal, + transferDate: isoInDays(0), + transferTime: '12:00', + trackingNumber: '987654321', + destinationAccountId: account.id, + receiptImage: bigBase64(400), + }, + }); + check('ثبت رسید (تصویر ۴۰۰KB)', receipt.status < 300, `status=${receipt.status} ${JSON.stringify(receipt.data).slice(0, 200)}`); + const afterReceipt = await api(`/orders/${cashOrderId}`, { token: userToken }); + check('وضعیت به تأیید پرداخت رفت', afterReceipt.data?.status === 'awaiting_cash_confirmation', `status=${afterReceipt.data?.status}`); + + const receipts = await api('/admin/cash-payments', { token: adminToken }); + const pendingReceipt = (receipts.data ?? []).find((r) => r.orderId === cashOrderId); + check('رسید در صف ادمین است', !!pendingReceipt, JSON.stringify(receipts.data)?.slice(0, 160)); + + const rejectReceipt = await api(`/admin/cash-payments/${pendingReceipt.id}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'rejected', note: 'مبلغ نامطابق' }, + }); + check('رد رسید', rejectReceipt.status < 300, `status=${rejectReceipt.status}`); + const afterReject = await api(`/orders/${cashOrderId}`, { token: userToken }); + check( + 'رد رسید مشتری را به مرحله پرداخت برمی‌گرداند', + afterReject.data?.status === 'awaiting_cash_payment', + `status=${afterReject.data?.status}`, + ); + + const approveReceipt = await api(`/admin/cash-payments/${pendingReceipt.id}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'approved' }, + }); + check('تأیید رسید', approveReceipt.status < 300, `status=${approveReceipt.status}`); + const afterApprove = await api(`/orders/${cashOrderId}`, { token: userToken }); + check('وضعیت پرداخت تأییدشده', afterApprove.data?.status === 'cash_confirmed', `status=${afterApprove.data?.status}`); + + const finalApprove = await api(`/admin/orders/${cashOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'final_approved' }, + }); + check('تأیید نهایی سفارش نقدی', finalApprove.status < 300, `status=${finalApprove.status}`); + + console.log('\n[11] مسیر چکی با حلقه اصلاح'); + await api('/carts', { method: 'DELETE', token: userToken }); + await api('/carts/items', { + method: 'POST', + token: userToken, + body: { productId: product.id, meterage }, + }); + const checkOrder = await api('/orders', { + method: 'POST', + token: userToken, + body: { companyId: company.id, items: [{ productId: product.id, meterage }] }, + }); + const checkOrderId = checkOrder.data?.id; + check('ثبت سفارش دوم', checkOrder.status < 300, `status=${checkOrder.status}`); + + const checkDetail = await api(`/admin/orders/${checkOrderId}`, { token: adminToken }); + const checkItem = checkDetail.data?.items?.[0]; + await api(`/admin/orders/${checkOrderId}/pricing`, { + method: 'PATCH', + token: adminToken, + body: { items: [{ id: checkItem.id, confirmedMeterage: meterage, unitPrice: 100000 }] }, + }); + await api(`/admin/orders/${checkOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'awaiting_settlement' }, + }); + const chooseCheck = await api(`/orders/${checkOrderId}/settlement`, { + method: 'POST', + token: userToken, + body: { settlementType: 'check', checkMode: 'full' }, + }); + check('انتخاب تسویه چکی', chooseCheck.status < 300, `status=${chooseCheck.status} ${JSON.stringify(chooseCheck.data).slice(0, 150)}`); + check('وضعیت در حال ثبت چک', chooseCheck.data?.status === 'check_in_progress', `status=${chooseCheck.data?.status}`); + + const submitChecks = await api(`/orders/${checkOrderId}/checks`, { + method: 'PATCH', + token: userToken, + body: { + checkMode: 'full', + checks: [ + checkPayload(expectedTotal, 30, { + frontImage: bigBase64(300), + backImage: bigBase64(300), + }), + ], + }, + }); + check('ارسال چک‌ها (بدنه ~۶۰۰KB)', submitChecks.status < 300, `status=${submitChecks.status} ${JSON.stringify(submitChecks.data).slice(0, 200)}`); + check('وضعیت بررسی چک', submitChecks.data?.status === 'awaiting_check_review', `status=${submitChecks.data?.status}`); + check('راس چک محاسبه شد', submitChecks.data?.checkRassDays === 30, `rass=${submitChecks.data?.checkRassDays}`); + + const orderChecks = await api(`/admin/orders/${checkOrderId}/checks`, { token: adminToken }); + const firstCheck = (orderChecks.data ?? [])[0]; + check('چک برای ادمین دیده می‌شود', !!firstCheck, JSON.stringify(orderChecks.data)?.slice(0, 160)); + + const rejectCheck = await api(`/admin/checks/${firstCheck.id}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'rejected', note: 'تصویر ناخوانا' }, + }); + check('رد چک', rejectCheck.status < 300, `status=${rejectCheck.status}`); + const afterCheckReject = await api(`/orders/${checkOrderId}`, { token: userToken }); + check( + 'رد چک به اصلاح می‌رود نه بن‌بست', + afterCheckReject.data?.status === 'check_needs_fix', + `status=${afterCheckReject.data?.status}`, + ); + + const fixChecks = await api(`/orders/${checkOrderId}/checks`, { + method: 'PATCH', + token: userToken, + body: { checkMode: 'full', checks: [checkPayload(expectedTotal, 45)] }, + }); + check('اصلاح چک‌ها', fixChecks.status < 300, `status=${fixChecks.status} ${JSON.stringify(fixChecks.data).slice(0, 200)}`); + check('بازگشت به بررسی چک', fixChecks.data?.status === 'awaiting_check_review', `status=${fixChecks.data?.status}`); + + const fixedChecks = await api(`/admin/orders/${checkOrderId}/checks`, { token: adminToken }); + const approveCheck = await api(`/admin/checks/${fixedChecks.data[0].id}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'approved' }, + }); + check('تأیید چک', approveCheck.status < 300, `status=${approveCheck.status}`); + const afterCheckApprove = await api(`/orders/${checkOrderId}`, { token: userToken }); + check('وضعیت تأیید چک‌ها', afterCheckApprove.data?.status === 'checks_approved', `status=${afterCheckApprove.data?.status}`); + + console.log('\n[12] تنظیمات سیستم'); + const settings = await api('/admin/settings', { token: adminToken }); + check('خواندن تنظیمات', settings.status < 300 && typeof settings.data?.maxMeteragePerInvoice === 'number', JSON.stringify(settings.data)?.slice(0, 160)); + const savedSettings = await api('/admin/settings', { + method: 'PATCH', + token: adminToken, + body: { smsPricingReady: false, maxMeteragePerInvoice: 6000 }, + }); + check('ذخیره تنظیمات', savedSettings.data?.maxMeteragePerInvoice === 6000 && savedSettings.data?.smsPricingReady === false, JSON.stringify(savedSettings.data)?.slice(0, 200)); + const companyAfter = await api('/companies/public'); + check( + 'سقف متراژ شرکت از تنظیمات ارث می‌برد', + companyAfter.data?.[0]?.maxMeteragePerInvoice === 6000, + `max=${companyAfter.data?.[0]?.maxMeteragePerInvoice}`, + ); + await api('/admin/settings', { + method: 'PATCH', + token: adminToken, + body: { smsPricingReady: true, maxMeteragePerInvoice: 5000 }, + }); + + console.log('\n[13] دسترسی متقاطع بین مشتری‌ها'); + const otherPhone = '09129999001'; + await api('/auth/otp/request', { + method: 'POST', + body: { phone: otherPhone, firstName: 'تست', lastName: 'دوم', isRegister: true }, + }); + const otherVerify = await api('/auth/otp/verify', { + method: 'POST', + body: { phone: otherPhone, code: String(code) }, + }); + const otherToken = otherVerify.data?.accessToken; + check('ثبت‌نام مشتری دوم', !!otherToken, JSON.stringify(otherVerify.data).slice(0, 120)); + if (otherToken) { + const foreign = await api(`/orders/${cashOrderId}`, { token: otherToken }); + check('سفارش دیگری قابل خواندن نیست', foreign.status === 403, `status=${foreign.status}`); + const foreignSettlement = await api(`/orders/${checkOrderId}/settlement`, { + method: 'POST', + token: otherToken, + body: { settlementType: 'cash' }, + }); + check('انتخاب تسویه روی سفارش دیگری رد می‌شود', foreignSettlement.status === 403, `status=${foreignSettlement.status}`); + const foreignNotifs = await api('/notifications', { token: otherToken }); + check('اعلان‌ها بین مشتری‌ها جدا است', (foreignNotifs.data ?? []).length === 0, `count=${(foreignNotifs.data ?? []).length}`); + } + + console.log('\n[14] محافظت از ویرایش بعد از تحویل'); + const forceDeliver = await api(`/admin/orders/${checkOrderId}/status`, { + method: 'PATCH', + token: adminToken, + body: { status: 'delivered', force: true, reason: 'تحویل دستی برای تست' }, + }); + check('گذر دستی با دلیل پذیرفته می‌شود', forceDeliver.status < 300, `status=${forceDeliver.status} ${JSON.stringify(forceDeliver.data).slice(0, 150)}`); + const lateEdit = await api(`/orders/${checkOrderId}/checks`, { + method: 'PATCH', + token: userToken, + body: { checks: [checkPayload(1000, 10)] }, + }); + check('ویرایش چک بعد از تحویل رد می‌شود', lateEdit.status === 400, `status=${lateEdit.status}`); + const lateReceipt = await api(`/orders/${checkOrderId}/cash-receipt`, { + method: 'POST', + token: userToken, + body: { + amount: 1000, + transferDate: isoInDays(0), + transferTime: '12:00', + trackingNumber: '1', + destinationAccountId: account.id, + }, + }); + check('رسید نقدی روی سفارش چکی رد می‌شود', lateReceipt.status === 400, `status=${lateReceipt.status}`); + + console.log(`\n== نتیجه: ${passed} موفق، ${failed} ناموفق ==\n`); + process.exit(failed === 0 ? 0 : 1); +} + +main().catch((e) => { + console.error('اجرای تست شکست خورد:', e); + process.exit(1); +}); diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..beab6b3 --- /dev/null +++ b/setup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +echo "==> Checking Docker..." +if ! docker info >/dev/null 2>&1; then + echo "Docker در دسترس نیست. کاربر را به گروه docker اضافه کنید:" + echo " sudo usermod -aG docker \"\$USER\"" + echo "سپس یک‌بار از سیستم خارج و دوباره وارد شوید." + exit 1 +fi + +echo "==> Starting PostgreSQL..." +docker compose up -d + +echo "==> Waiting for Postgres health..." +for i in $(seq 1 30); do + if docker compose exec -T postgres pg_isready -U fabric -d fabric_trade >/dev/null 2>&1; then + break + fi + sleep 1 +done + +echo "==> Installing deps (if needed)..." +npm install + +echo "==> Prisma generate + migrate + seed..." +npx prisma generate +npx prisma migrate deploy +npm run prisma:seed + +echo "" +echo "آماده است. سرور را با این دستور اجرا کنید:" +echo " npm run start:dev" +echo "" +echo "Swagger: http://localhost:3000/api/docs" +echo "Admin: 09128081392 / 123456" +echo "OTP: 09121234567 / 1234" diff --git a/src/admin-users/admin-users.controller.ts b/src/admin-users/admin-users.controller.ts new file mode 100644 index 0000000..94a1bd5 --- /dev/null +++ b/src/admin-users/admin-users.controller.ts @@ -0,0 +1,40 @@ +import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; +import { AdminUsersService } from './admin-users.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { + CreateAdminUserDto, + SetAdminActiveDto, + UpdateAdminUserDto, +} from './dto/admin-user.dto'; + +@Controller('admin/users') +@AuthType('admin') +@Roles('admin') +export class AdminUsersController { + constructor(private readonly adminUsersService: AdminUsersService) {} + + @Get() + findAll() { + return this.adminUsersService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.adminUsersService.findOne(id); + } + + @Post() + create(@Body() dto: CreateAdminUserDto) { + return this.adminUsersService.create(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateAdminUserDto) { + return this.adminUsersService.update(id, dto); + } + + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetAdminActiveDto) { + return this.adminUsersService.setActive(id, dto.isActive); + } +} diff --git a/src/admin-users/admin-users.module.ts b/src/admin-users/admin-users.module.ts new file mode 100644 index 0000000..f7da5c6 --- /dev/null +++ b/src/admin-users/admin-users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AdminUsersController } from './admin-users.controller'; +import { AdminUsersService } from './admin-users.service'; + +@Module({ + controllers: [AdminUsersController], + providers: [AdminUsersService], + exports: [AdminUsersService], +}) +export class AdminUsersModule {} diff --git a/src/admin-users/admin-users.service.ts b/src/admin-users/admin-users.service.ts new file mode 100644 index 0000000..a6228ea --- /dev/null +++ b/src/admin-users/admin-users.service.ts @@ -0,0 +1,154 @@ +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { AdminRole, Prisma } from '@prisma/client'; +import * as bcrypt from 'bcryptjs'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateAdminUserDto, UpdateAdminUserDto } from './dto/admin-user.dto'; +import { + isValidPhone, + normalizePhone, + toEnglishDigits, +} from '../common/utils/phone'; + +const DEFAULT_PASSWORD = '123456'; + +@Injectable() +export class AdminUsersService { + constructor(private readonly prisma: PrismaService) {} + + async findAll() { + const users = await this.prisma.adminUser.findMany({ + orderBy: { createdAt: 'desc' }, + }); + return users.map((u) => this.map(u)); + } + + async findOne(id: string) { + const user = await this.prisma.adminUser.findUnique({ where: { id } }); + if (!user) throw new NotFoundException('کاربر ادمین یافت نشد'); + return this.map(user); + } + + async create(dto: CreateAdminUserDto) { + const phone = normalizePhone(toEnglishDigits(dto.phone)); + if (!isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + const passwordHash = await bcrypt.hash( + dto.password?.trim() || DEFAULT_PASSWORD, + 10, + ); + + const email = + dto.email?.trim().toLowerCase() || + `${phone.replace(/\D/g, '')}@noemail.local` + + try { + const user = await this.prisma.adminUser.create({ + data: { + fullName: dto.fullName.trim(), + phone, + email, + passwordHash, + role: dto.role ?? AdminRole.admin, + isActive: dto.isActive ?? true, + }, + }); + return this.map(user); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('شماره یا ایمیل تکراری است'); + } + throw e; + } + } + + async update(id: string, dto: UpdateAdminUserDto) { + await this.ensure(id); + const phone = + dto.phone !== undefined + ? normalizePhone(toEnglishDigits(dto.phone)) + : undefined; + if (phone !== undefined && !isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + + const data: Prisma.AdminUserUpdateInput = { + fullName: dto.fullName?.trim(), + phone, + email: + dto.email === undefined + ? undefined + : dto.email.trim() + ? dto.email.trim().toLowerCase() + : undefined, + role: dto.role, + isActive: dto.isActive, + }; + if (dto.password?.trim()) { + data.passwordHash = await bcrypt.hash(dto.password.trim(), 10); + } + + try { + const user = await this.prisma.adminUser.update({ + where: { id }, + data, + }); + return this.map(user); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('شماره یا ایمیل تکراری است'); + } + throw e; + } + } + + async setActive(id: string, isActive: boolean) { + await this.ensure(id); + const user = await this.prisma.adminUser.update({ + where: { id }, + data: { isActive }, + }); + return this.map(user); + } + + private async ensure(id: string) { + const user = await this.prisma.adminUser.findUnique({ where: { id } }); + if (!user) throw new NotFoundException('کاربر ادمین یافت نشد'); + return user; + } + + private map(user: { + id: string; + fullName: string; + phone: string; + email: string; + role: AdminRole; + isActive: boolean; + lastLoginAt: Date | null; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: user.id, + fullName: user.fullName, + phone: user.phone, + email: user.email, + role: user.role, + isActive: user.isActive, + lastLoginAt: user.lastLoginAt?.toISOString() ?? null, + createdAt: user.createdAt.toISOString(), + updatedAt: user.updatedAt.toISOString(), + }; + } +} diff --git a/src/admin-users/dto/admin-user.dto.ts b/src/admin-users/dto/admin-user.dto.ts new file mode 100644 index 0000000..607b496 --- /dev/null +++ b/src/admin-users/dto/admin-user.dto.ts @@ -0,0 +1,69 @@ +import { + IsBoolean, + IsEmail, + IsEnum, + IsOptional, + IsString, + MinLength, +} from 'class-validator'; +import { AdminRole } from '@prisma/client'; + +export class CreateAdminUserDto { + @IsString() + @MinLength(1) + fullName!: string; + + @IsString() + @MinLength(10) + phone!: string; + + @IsOptional() + @IsEmail() + email?: string; + + @IsOptional() + @IsString() + @MinLength(4) + password?: string; + + @IsOptional() + @IsEnum(AdminRole) + role?: AdminRole; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateAdminUserDto { + @IsOptional() + @IsString() + @MinLength(1) + fullName?: string; + + @IsOptional() + @IsString() + phone?: string; + + @IsOptional() + @IsEmail() + email?: string; + + @IsOptional() + @IsString() + @MinLength(4) + password?: string; + + @IsOptional() + @IsEnum(AdminRole) + role?: AdminRole; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class SetAdminActiveDto { + @IsBoolean() + isActive!: boolean; +} diff --git a/src/agent/agent.controller.ts b/src/agent/agent.controller.ts new file mode 100644 index 0000000..15f93d9 --- /dev/null +++ b/src/agent/agent.controller.ts @@ -0,0 +1,119 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { AgentService } from './agent.service'; +import { + CompleteReminderDto, + CreateAgentCustomerDto, + CreateMeetingDto, + CreateReminderDto, +} from './dto/agent.dto'; + +@Controller() +export class AgentController { + constructor(private readonly agentService: AgentService) {} + + @AuthType('any') + @Get('catalog/categories') + categories() { + return this.agentService.listCategories(); + } + + @AuthType('customer') + @Roles('agent') + @Get('agent/profile') + profile(@CurrentUser() user: AuthPrincipal) { + return this.agentService.getProfile(user.id); + } + + @AuthType('customer') + @Roles('agent') + @Get('agent/customers') + customers( + @CurrentUser() user: AuthPrincipal, + @Query('q') q?: string, + @Query('status') status?: string, + ) { + return this.agentService.listMyCustomers(user.id, q, status); + } + + @AuthType('customer') + @Roles('agent') + @Post('agent/customers') + createCustomer( + @CurrentUser() user: AuthPrincipal, + @Body() dto: CreateAgentCustomerDto, + ) { + return this.agentService.createCustomer(user.id, dto); + } + + @AuthType('customer') + @Roles('agent') + @Get('agent/customers/:id') + customerDetail( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + ) { + return this.agentService.getCustomerDetail(user.id, id); + } + + @AuthType('customer') + @Roles('agent') + @Post('agent/meetings') + createMeeting( + @CurrentUser() user: AuthPrincipal, + @Body() dto: CreateMeetingDto, + ) { + return this.agentService.createMeeting(user.id, dto); + } + + @AuthType('customer') + @Roles('agent') + @Get('agent/reminders') + reminders( + @CurrentUser() user: AuthPrincipal, + @Query('filter') filter?: string, + ) { + return this.agentService.listReminders(user.id, filter); + } + + @AuthType('customer') + @Roles('agent') + @Get('agent/reminders/:id') + reminderDetail( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + ) { + return this.agentService.getReminder(user.id, id); + } + + @AuthType('customer') + @Roles('agent') + @Post('agent/reminders') + createReminder( + @CurrentUser() user: AuthPrincipal, + @Body() dto: CreateReminderDto, + ) { + return this.agentService.createReminder(user.id, dto); + } + + @AuthType('customer') + @Roles('agent') + @Patch('agent/reminders/:id/complete') + completeReminder( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: CompleteReminderDto, + ) { + return this.agentService.completeReminder(user.id, id, dto); + } +} diff --git a/src/agent/agent.module.ts b/src/agent/agent.module.ts new file mode 100644 index 0000000..4a8524a --- /dev/null +++ b/src/agent/agent.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AgentController } from './agent.controller'; +import { AgentService } from './agent.service'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [AgentController], + providers: [AgentService], + exports: [AgentService], +}) +export class AgentModule {} diff --git a/src/agent/agent.service.ts b/src/agent/agent.service.ts new file mode 100644 index 0000000..8418b1e --- /dev/null +++ b/src/agent/agent.service.ts @@ -0,0 +1,468 @@ +import { + BadRequestException, + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { AppUserRole, InteractionType, Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + isValidPhone, + normalizePhone, + toEnglishDigits, +} from '../common/utils/phone'; +import { + CompleteReminderDto, + CreateAgentCustomerDto, + CreateMeetingDto, + CreateReminderDto, +} from './dto/agent.dto'; +import { loadCatalogCategories } from '../catalog/category-defs'; + +const AVATAR_COLORS = ['#f9a8d4', '#c4b5fd', '#93c5fd', '#86efac', '#fcd34d', '#fdba74']; + +@Injectable() +export class AgentService { + constructor(private readonly prisma: PrismaService) {} + + private async requireAgent(agentId: string) { + const agent = await this.prisma.customer.findUnique({ + where: { id: agentId }, + }); + if (!agent || agent.appRole !== AppUserRole.agent) { + throw new ForbiddenException('فقط واسطه به این بخش دسترسی دارد'); + } + return agent; + } + + async getProfile(agentId: string) { + 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: string, q?: string, status?: string) { + await this.requireAgent(agentId); + const where: Prisma.CustomerWhereInput = { + assignedAgentId: agentId, + ...(status === 'inactive' ? { isActive: false } : {}), + ...(status === 'active' || status === 'new' ? { isActive: true } : {}), + ...(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 }, + }, + }, + }); + + const mapped = 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') as 'active' | 'inactive' | 'new', + 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 as string | undefined, + agentName: undefined as string | undefined, + }; + }); + + if (status === 'active' || status === 'new' || status === 'inactive') { + return mapped.filter((c) => c.status === status); + } + return mapped; + } + + async createCustomer(agentId: string, dto: CreateAgentCustomerDto) { + await this.requireAgent(agentId); + + const phone = normalizePhone(toEnglishDigits(dto.phone)); + if (!isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + + const firstName = dto.firstName.trim(); + const lastName = dto.lastName.trim(); + const businessName = dto.businessName?.trim() || null; + if (firstName.length < 2) { + throw new BadRequestException('نام الزامی است'); + } + if (lastName.length < 2) { + throw new BadRequestException('نام خانوادگی الزامی است'); + } + + const existing = await this.prisma.customer.findUnique({ where: { phone } }); + if (existing) { + if (existing.assignedAgentId === agentId) { + throw new ConflictException('این مشتری از قبل در لیست شماست'); + } + throw new ConflictException( + 'این شماره قبلاً ثبت شده است. برای انتساب به لیست خود، از ادمین درخواست کنید.', + ); + } + + const colorIndex = + phone.split('').reduce((sum, ch) => sum + ch.charCodeAt(0), 0) % + AVATAR_COLORS.length; + const avatarColor = AVATAR_COLORS[colorIndex] ?? '#c4b5fd'; + + try { + const created = await this.prisma.customer.create({ + data: { + firstName, + lastName, + phone, + businessName, + assignedAgentId: agentId, + appRole: AppUserRole.customer, + isActive: true, + avatarLetter: firstName.charAt(0), + avatarColor, + }, + }); + + return { + id: created.id, + name: created.businessName || `${created.firstName} ${created.lastName}`, + contactName: `${created.firstName} ${created.lastName}`, + phone: created.phone, + status: 'new' as const, + avatarLetter: created.avatarLetter || created.firstName.charAt(0), + avatarColor: created.avatarColor || avatarColor, + totalPurchaseToman: 0, + orderCount: 0, + averageOrderToman: 0, + levelLabel: created.levelLabel || 'برنزی', + firstPurchaseDate: '—', + }; + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('این شماره قبلاً ثبت شده است'); + } + throw e; + } + } + + async getCustomerDetail(agentId: string, customerId: string) { + 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 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' as const, + 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: string, dto: CreateMeetingDto) { + await this.requireAgent(agentId); + const customer = await this.prisma.customer.findFirst({ + where: { id: dto.customerId, assignedAgentId: agentId }, + }); + if (!customer) throw new 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: string, filter?: string) { + await this.requireAgent(agentId); + const where: Prisma.ReminderWhereInput = { ownerId: agentId }; + + const today = new Date(); + const yyyy = today.getFullYear(); + const mm = String(today.getMonth() + 1).padStart(2, '0'); + const dd = String(today.getDate()).padStart(2, '0'); + const todayStr = `${yyyy}-${mm}-${dd}`; + + const weekEnd = new Date(today); + weekEnd.setDate(weekEnd.getDate() + 7); + const weekEndStr = `${weekEnd.getFullYear()}-${String(weekEnd.getMonth() + 1).padStart(2, '0')}-${String(weekEnd.getDate()).padStart(2, '0')}`; + + if (filter === 'past') { + where.completed = true; + } else if (filter === 'today') { + where.completed = false; + where.date = todayStr; + } else if (filter === 'week') { + where.completed = false; + where.date = { gte: todayStr, lte: weekEndStr }; + } else if (filter === 'all') { + // no extra filters + } 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: string, reminderId: string) { + await this.requireAgent(agentId); + const reminder = await this.prisma.reminder.findFirst({ + where: { id: reminderId, ownerId: agentId }, + include: { customer: true }, + }); + if (!reminder) throw new 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: string, dto: CreateReminderDto) { + await this.requireAgent(agentId); + const customer = await this.prisma.customer.findFirst({ + where: { id: dto.customerId, assignedAgentId: agentId }, + }); + if (!customer) throw new 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 ?? InteractionType.note, + }, + }); + } + + async completeReminder( + agentId: string, + reminderId: string, + dto: CompleteReminderDto, + ) { + await this.requireAgent(agentId); + const reminder = await this.prisma.reminder.findFirst({ + where: { id: reminderId, ownerId: agentId }, + }); + if (!reminder) throw new NotFoundException('یادآور یافت نشد'); + const completed = dto.completed ?? true; + return this.prisma.reminder.update({ + where: { id: reminderId }, + data: { + completed, + completedAt: completed ? new Date() : null, + }, + }); + } + + async listCategories() { + return loadCatalogCategories(this.prisma); + } +} diff --git a/src/agent/dto/agent.dto.ts b/src/agent/dto/agent.dto.ts new file mode 100644 index 0000000..111aa5d --- /dev/null +++ b/src/agent/dto/agent.dto.ts @@ -0,0 +1,112 @@ +import { + IsBoolean, + IsEnum, + IsOptional, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; +import { InteractionType } from '@prisma/client'; + +export class CreateMeetingDto { + @IsString() + customerId!: string; + + @IsEnum(InteractionType) + type!: InteractionType; + + @IsString() + @MinLength(2) + title!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + subject?: string; + + @IsOptional() + @IsString() + @MaxLength(1000) + content?: string; + + @IsString() + date!: string; + + @IsOptional() + @IsString() + time?: string; + + @IsOptional() + @IsBoolean() + createReminder?: boolean; + + @IsOptional() + @IsString() + reminderTitle?: string; + + @IsOptional() + @IsString() + reminderDate?: string; + + @IsOptional() + @IsString() + reminderTime?: string; + + @IsOptional() + @IsString() + reminderNote?: string; +} + +export class CreateReminderDto { + @IsString() + customerId!: string; + + @IsString() + @MinLength(2) + title!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + note?: string; + + @IsString() + date!: string; + + @IsString() + time!: string; + + @IsOptional() + @IsEnum(InteractionType) + interactionType?: InteractionType; +} + +export class CompleteReminderDto { + @IsOptional() + @IsBoolean() + completed?: boolean; +} + +export class CreateAgentCustomerDto { + @IsString() + @MinLength(2) + firstName!: string; + + @IsString() + @MinLength(2) + lastName!: string; + + @IsString() + @MinLength(10) + phone!: string; + + @IsOptional() + @IsString() + businessName?: string; +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..ae970ed --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,63 @@ +import { Module } from '@nestjs/common'; +import { APP_GUARD } from '@nestjs/core'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; +import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; +import { CompaniesModule } from './companies/companies.module'; +import { CatalogsModule } from './catalogs/catalogs.module'; +import { ProductsModule } from './products/products.module'; +import { CustomersModule } from './customers/customers.module'; +import { AdminUsersModule } from './admin-users/admin-users.module'; +import { OrdersModule } from './orders/orders.module'; +import { ChecksModule } from './checks/checks.module'; +import { CashPaymentsModule } from './cash-payments/cash-payments.module'; +import { ShipmentsModule } from './shipments/shipments.module'; +import { DashboardModule } from './dashboard/dashboard.module'; +import { SettingsModule } from './settings/settings.module'; +import { CartsModule } from './carts/carts.module'; +import { NotificationsModule } from './notifications/notifications.module'; +import { AgentModule } from './agent/agent.module'; +import { CatalogRequestsModule } from './catalog-requests/catalog-requests.module'; +import { BrandsModule } from './brands/brands.module'; +import { LevelsModule } from './levels/levels.module'; +import { SuggestionsModule } from './suggestions/suggestions.module'; +import { StockNotifyModule } from './stock-notify/stock-notify.module'; +import { BannersModule } from './banners/banners.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + ScheduleModule.forRoot(), + PrismaModule, + AuthModule, + CompaniesModule, + CatalogsModule, + ProductsModule, + CustomersModule, + AdminUsersModule, + OrdersModule, + ChecksModule, + CashPaymentsModule, + ShipmentsModule, + DashboardModule, + SettingsModule, + CartsModule, + NotificationsModule, + AgentModule, + CatalogRequestsModule, + BrandsModule, + LevelsModule, + SuggestionsModule, + StockNotifyModule, + BannersModule, + ], + providers: [ + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], +}) +export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..adeea01 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,55 @@ +import { Body, Controller, Get, Patch, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { + AdminLoginDto, + RequestOtpDto, + UpdateProfileDto, + VerifyOtpDto, +} from './dto/auth.dto'; +import { AuthType, Public } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; + +@Controller() +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Public() + @Get('health') + health() { + return { ok: true, service: 'fabric-trade-api' }; + } + + @Public() + @Post('auth/otp/request') + requestOtp(@Body() dto: RequestOtpDto) { + return this.authService.requestOtp(dto); + } + + @Public() + @Post('auth/otp/verify') + verifyOtp(@Body() dto: VerifyOtpDto) { + return this.authService.verifyOtp(dto); + } + + @Public() + @Post('admin/auth/login') + adminLogin(@Body() dto: AdminLoginDto) { + return this.authService.adminLogin(dto); + } + + @AuthType('any') + @Get('auth/me') + me(@CurrentUser() user: AuthPrincipal) { + return this.authService.me(user.id, user.type); + } + + @AuthType('customer') + @Patch('auth/profile') + updateProfile( + @CurrentUser() user: AuthPrincipal, + @Body() dto: UpdateProfileDto, + ) { + return this.authService.updateProfile(user.id, dto); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..667ee01 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { SmsModule } from '../sms/sms.module'; +import { resolveJwtSecret } from '../common/utils/jwt-secret'; + +@Module({ + imports: [ + SmsModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: resolveJwtSecret(config), + signOptions: { + expiresIn: config.get('JWT_EXPIRES_IN', '7d'), + }, + }), + }), + ], + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..d7a5958 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,302 @@ +import { + BadRequestException, + Injectable, + ServiceUnavailableException, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { randomInt } from 'node:crypto'; +import * as bcrypt from 'bcryptjs'; +import { PrismaService } from '../prisma/prisma.service'; +import { SmsService } from '../sms/sms.service'; +import { + AdminLoginDto, + RequestOtpDto, + UpdateProfileDto, + VerifyOtpDto, +} from './dto/auth.dto'; +import { + isValidPhone, + normalizePhone, + toEnglishDigits, +} from '../common/utils/phone'; + +const MAX_OTP_ATTEMPTS = 5; + +@Injectable() +export class AuthService { + constructor( + private readonly prisma: PrismaService, + private readonly jwt: JwtService, + private readonly config: ConfigService, + private readonly sms: SmsService, + ) {} + + async requestOtp(dto: RequestOtpDto) { + const phone = normalizePhone(toEnglishDigits(dto.phone)); + if (!isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + + const existing = await this.prisma.customer.findUnique({ + where: { phone }, + }); + const isRegister = Boolean(dto.isRegister); + + if (isRegister) { + if (!dto.firstName?.trim() || !dto.lastName?.trim()) { + throw new BadRequestException('نام و نام خانوادگی الزامی است'); + } + if (existing) { + throw new BadRequestException('این شماره قبلاً ثبت شده است'); + } + } else if (!existing) { + throw new BadRequestException( + 'کاربری با این مشخصات وجود ندارد؛ لطفاً ثبت‌نام کنید', + ); + } else if (!existing.isActive) { + throw new UnauthorizedException('حساب کاربری شما غیرفعال است'); + } + + const smsEnabled = this.sms.isEnabled(); + const code = smsEnabled + ? String(randomInt(1000, 10000)) + : this.config.get('MOCK_OTP_CODE', '1234'); + const ttl = Number(this.config.get('OTP_TTL_MINUTES', 5)); + const expiresAt = new Date(Date.now() + ttl * 60_000); + + const challenge = await this.prisma.$transaction(async (tx) => { + await tx.otpChallenge.deleteMany({ where: { phone } }); + return tx.otpChallenge.create({ + data: { + phone, + code, + firstName: dto.firstName?.trim(), + lastName: dto.lastName?.trim(), + isRegister, + expiresAt, + }, + }); + }); + + if (smsEnabled) { + const sent = await this.sms.send(phone, this.sms.buildOtpMessage(code)); + if (!sent.success) { + // چالش ناموفق را پاک می‌کنیم تا کاربر با کدی که هرگز نرسیده گیر نکند + await this.prisma.otpChallenge.deleteMany({ + where: { id: challenge.id }, + }); + throw new ServiceUnavailableException( + 'ارسال پیامک با خطا مواجه شد؛ دوباره تلاش کنید', + ); + } + } + + return { + success: true, + message: 'کد تأیید ارسال شد', + expiresAt, + ...(this.exposesDebugCode(smsEnabled) ? { debugCode: code } : {}), + }; + } + + /** کد فقط در محیط توسعه و وقتی پیامک خاموش است برگردانده می‌شود. */ + private exposesDebugCode(smsEnabled: boolean): boolean { + if (smsEnabled) return false; + return this.config.get('NODE_ENV') !== 'production'; + } + + async verifyOtp(dto: VerifyOtpDto) { + const phone = normalizePhone(toEnglishDigits(dto.phone)); + const code = toEnglishDigits(dto.code).trim(); + + const challenge = await this.prisma.otpChallenge.findFirst({ + where: { phone }, + orderBy: { createdAt: 'desc' }, + }); + + if (!challenge || challenge.expiresAt < new Date()) { + throw new UnauthorizedException('کد منقضی شده است'); + } + if (challenge.attempts >= MAX_OTP_ATTEMPTS) { + await this.prisma.otpChallenge.deleteMany({ where: { phone } }); + throw new UnauthorizedException( + 'تعداد تلاش‌های نادرست زیاد است؛ کد جدید بگیرید', + ); + } + if (challenge.code !== code) { + await this.prisma.otpChallenge.update({ + where: { id: challenge.id }, + data: { attempts: { increment: 1 } }, + }); + throw new UnauthorizedException('کد تأیید نادرست است'); + } + + let user = await this.prisma.customer.findUnique({ where: { phone } }); + if (challenge.isRegister) { + if (user) { + throw new BadRequestException('این شماره قبلاً ثبت شده است'); + } + user = await this.prisma.customer.create({ + data: { + phone, + firstName: challenge.firstName ?? 'کاربر', + lastName: challenge.lastName ?? 'جدید', + }, + }); + } else if (!user) { + throw new UnauthorizedException('کاربر یافت نشد'); + } else if (!user.isActive) { + throw new UnauthorizedException('حساب کاربری شما غیرفعال است'); + } + + await this.prisma.otpChallenge.deleteMany({ where: { phone } }); + + const accessToken = await this.signCustomerToken( + user.id, + user.phone, + user.appRole, + ); + return { + accessToken, + user: this.mapCustomer(user), + }; + } + + async adminLogin(dto: AdminLoginDto) { + const phone = normalizePhone(toEnglishDigits(dto.phone)); + const admin = await this.prisma.adminUser.findUnique({ where: { phone } }); + if (!admin || !admin.isActive) { + throw new UnauthorizedException('نام کاربری یا رمز عبور نادرست است'); + } + + const ok = await bcrypt.compare(dto.password, admin.passwordHash); + if (!ok) { + throw new UnauthorizedException('نام کاربری یا رمز عبور نادرست است'); + } + + await this.prisma.adminUser.update({ + where: { id: admin.id }, + data: { lastLoginAt: new Date() }, + }); + + const accessToken = await this.jwt.signAsync({ + sub: admin.id, + type: 'admin', + role: admin.role, + phone: admin.phone, + }); + + return { + accessToken, + user: { + id: admin.id, + fullName: admin.fullName, + phone: admin.phone, + email: admin.email, + role: admin.role, + }, + }; + } + + async me(userId: string, type: 'customer' | 'admin') { + if (type === 'admin') { + const admin = await this.prisma.adminUser.findUnique({ + where: { id: userId }, + }); + if (!admin) throw new UnauthorizedException(); + return { + id: admin.id, + fullName: admin.fullName, + phone: admin.phone, + email: admin.email, + role: admin.role, + }; + } + + const user = await this.prisma.customer.findUnique({ + where: { id: userId }, + }); + if (!user) throw new UnauthorizedException(); + return this.mapCustomer(user); + } + + async updateProfile(userId: string, dto: UpdateProfileDto) { + const data: { + firstName?: string; + lastName?: string; + nationalId?: string | null; + avatarUrl?: string | null; + } = {}; + + if (dto.firstName !== undefined) data.firstName = dto.firstName.trim(); + if (dto.lastName !== undefined) data.lastName = dto.lastName.trim(); + if (dto.nationalId !== undefined) data.nationalId = dto.nationalId.trim() || null; + if (dto.avatarUrl !== undefined) data.avatarUrl = this.normalizeAvatar(dto.avatarUrl); + + const user = await this.prisma.customer.update({ + where: { id: userId }, + data, + }); + return this.mapCustomer(user); + } + + private normalizeAvatar(value: string | null): string | null { + if (value == null || value === '') return null; + if (!value.startsWith('data:image/')) { + throw new BadRequestException('فرمت تصویر پروفایل نامعتبر است.'); + } + if (value.length > 900_000) { + throw new BadRequestException('حجم تصویر پروفایل خیلی زیاد است.'); + } + return value; + } + + private async signCustomerToken( + id: string, + phone: string, + appRole: string = 'customer', + ) { + return this.jwt.signAsync({ + sub: id, + type: 'customer', + role: appRole, + phone, + }); + } + + private mapCustomer(user: { + id: string; + firstName: string; + lastName: string; + phone: string; + nationalId: string | null; + createdAt: Date; + appRole?: string; + agentCode?: string | null; + levelLabel?: string | null; + nextLevelLabel?: string | null; + pointsToNext?: number | null; + levelProgress?: number | null; + businessName?: string | null; + avatarUrl?: string | null; + }) { + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + phone: user.phone, + nationalId: user.nationalId ?? undefined, + createdAt: user.createdAt.toISOString(), + appRole: user.appRole ?? 'customer', + agentCode: user.agentCode ?? undefined, + levelLabel: user.levelLabel ?? undefined, + nextLevelLabel: user.nextLevelLabel ?? undefined, + pointsToNext: user.pointsToNext ?? undefined, + levelProgress: user.levelProgress ?? undefined, + businessName: user.businessName ?? undefined, + avatarUrl: user.avatarUrl ?? undefined, + }; + } +} diff --git a/src/auth/dto/auth.dto.ts b/src/auth/dto/auth.dto.ts new file mode 100644 index 0000000..fb42a76 --- /dev/null +++ b/src/auth/dto/auth.dto.ts @@ -0,0 +1,59 @@ +import { + IsBoolean, + IsOptional, + IsString, + Length, + MinLength, +} from 'class-validator'; + +export class RequestOtpDto { + @IsString() + phone!: string; + + @IsOptional() + @IsString() + firstName?: string; + + @IsOptional() + @IsString() + lastName?: string; + + @IsOptional() + @IsBoolean() + isRegister?: boolean; +} + +export class VerifyOtpDto { + @IsString() + phone!: string; + + @IsString() + @Length(4, 4) + code!: string; +} + +export class AdminLoginDto { + @IsString() + phone!: string; + + @IsString() + @MinLength(4) + password!: string; +} + +export class UpdateProfileDto { + @IsOptional() + @IsString() + firstName?: string; + + @IsOptional() + @IsString() + lastName?: string; + + @IsOptional() + @IsString() + nationalId?: string; + + @IsOptional() + avatarUrl?: string | null; +} diff --git a/src/banners/banners.controller.ts b/src/banners/banners.controller.ts new file mode 100644 index 0000000..5cfc71f --- /dev/null +++ b/src/banners/banners.controller.ts @@ -0,0 +1,52 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { BannersService } from './banners.service'; +import { CreateBannerDto, UpdateBannerDto } from './dto/banner.dto'; + +@Controller() +export class BannersController { + constructor(private readonly bannersService: BannersService) {} + + @AuthType('any') + @Get('banners') + publicList(@Query('slot') slot?: string) { + return this.bannersService.list(slot, true); + } + + @AuthType('admin') + @Roles('admin', 'sales') + @Get('admin/banners') + adminList(@Query('slot') slot?: string) { + return this.bannersService.list(slot); + } + + @AuthType('admin') + @Roles('admin', 'sales') + @Post('admin/banners') + create(@Body() dto: CreateBannerDto) { + return this.bannersService.create(dto); + } + + @AuthType('admin') + @Roles('admin', 'sales') + @Patch('admin/banners/:id') + update(@Param('id') id: string, @Body() dto: UpdateBannerDto) { + return this.bannersService.update(id, dto); + } + + @AuthType('admin') + @Roles('admin', 'sales') + @Delete('admin/banners/:id') + remove(@Param('id') id: string) { + return this.bannersService.remove(id); + } +} diff --git a/src/banners/banners.module.ts b/src/banners/banners.module.ts new file mode 100644 index 0000000..5314f8c --- /dev/null +++ b/src/banners/banners.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BannersController } from './banners.controller'; +import { BannersService } from './banners.service'; + +@Module({ + controllers: [BannersController], + providers: [BannersService], + exports: [BannersService], +}) +export class BannersModule {} diff --git a/src/banners/banners.service.ts b/src/banners/banners.service.ts new file mode 100644 index 0000000..ecacb2e --- /dev/null +++ b/src/banners/banners.service.ts @@ -0,0 +1,126 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateBannerDto, UpdateBannerDto } from './dto/banner.dto'; + +const ALLOWED_SLOTS = new Set(['home_hero', 'products_hero', 'categories_hero']); + +@Injectable() +export class BannersService { + constructor(private readonly prisma: PrismaService) {} + + async list(slot?: string, activeOnly = false) { + const rows = await this.prisma.appBanner.findMany({ + where: { + ...(slot ? { slot } : {}), + ...(activeOnly ? { isActive: true } : {}), + }, + orderBy: [{ slot: 'asc' }, { displayOrder: 'asc' }, { createdAt: 'desc' }], + }); + return rows.map((row) => this.map(row)); + } + + async create(dto: CreateBannerDto) { + const slot = this.normalizeSlot(dto.slot); + const banner = await this.prisma.appBanner.create({ + data: { + slot, + title: dto.title.trim(), + subtitle: dto.subtitle?.trim() || null, + ctaLabel: dto.ctaLabel?.trim() || null, + ctaLink: dto.ctaLink?.trim() || null, + imageUrl: this.normalizeImage(dto.imageUrl), + displayOrder: dto.displayOrder ?? 0, + isActive: dto.isActive ?? true, + }, + }); + return this.map(banner); + } + + async update(id: string, dto: UpdateBannerDto) { + await this.ensure(id); + const banner = await this.prisma.appBanner.update({ + where: { id }, + data: { + slot: dto.slot === undefined ? undefined : this.normalizeSlot(dto.slot), + title: dto.title?.trim(), + subtitle: + dto.subtitle === undefined ? undefined : dto.subtitle?.trim() || null, + ctaLabel: + dto.ctaLabel === undefined ? undefined : dto.ctaLabel?.trim() || null, + ctaLink: + dto.ctaLink === undefined ? undefined : dto.ctaLink?.trim() || null, + imageUrl: + dto.imageUrl === undefined + ? undefined + : this.normalizeImage(dto.imageUrl), + displayOrder: dto.displayOrder, + isActive: dto.isActive, + }, + }); + return this.map(banner); + } + + async remove(id: string) { + await this.ensure(id); + await this.prisma.appBanner.delete({ where: { id } }); + return { ok: true }; + } + + private async ensure(id: string) { + const row = await this.prisma.appBanner.findUnique({ where: { id } }); + if (!row) throw new NotFoundException('بنر یافت نشد'); + return row; + } + + private normalizeSlot(slot: string) { + const value = slot.trim(); + if (!ALLOWED_SLOTS.has(value)) { + throw new BadRequestException('بخش بنر نامعتبر است'); + } + return value; + } + + private normalizeImage(value?: string | null) { + if (value == null || value === '') return null; + const image = value.trim(); + if ( + !image.startsWith('data:image/') && + !image.startsWith('https://') && + !image.startsWith('http://') + ) { + throw new BadRequestException('فرمت تصویر بنر نامعتبر است'); + } + if (image.startsWith('data:image/') && image.length > 1_600_000) { + throw new BadRequestException('حجم تصویر بنر خیلی زیاد است'); + } + return image; + } + + private map(row: { + id: string; + slot: string; + title: string; + subtitle: string | null; + ctaLabel: string | null; + ctaLink: string | null; + imageUrl: string | null; + displayOrder: number; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: row.id, + slot: row.slot, + title: row.title, + subtitle: row.subtitle ?? '', + ctaLabel: row.ctaLabel ?? '', + ctaLink: row.ctaLink ?? '', + imageUrl: row.imageUrl ?? '', + displayOrder: row.displayOrder, + isActive: row.isActive, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; + } +} diff --git a/src/banners/dto/banner.dto.ts b/src/banners/dto/banner.dto.ts new file mode 100644 index 0000000..b137c47 --- /dev/null +++ b/src/banners/dto/banner.dto.ts @@ -0,0 +1,83 @@ +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateBannerDto { + @IsString() + @MinLength(2) + slot!: string; + + @IsString() + @MinLength(2) + title!: string; + + @IsOptional() + @IsString() + subtitle?: string | null; + + @IsOptional() + @IsString() + ctaLabel?: string | null; + + @IsOptional() + @IsString() + ctaLink?: string | null; + + @IsOptional() + @IsString() + imageUrl?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateBannerDto { + @IsOptional() + @IsString() + @MinLength(2) + slot?: string; + + @IsOptional() + @IsString() + @MinLength(2) + title?: string; + + @IsOptional() + @IsString() + subtitle?: string | null; + + @IsOptional() + @IsString() + ctaLabel?: string | null; + + @IsOptional() + @IsString() + ctaLink?: string | null; + + @IsOptional() + @IsString() + imageUrl?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/brands/brands.controller.ts b/src/brands/brands.controller.ts new file mode 100644 index 0000000..9eafdbf --- /dev/null +++ b/src/brands/brands.controller.ts @@ -0,0 +1,49 @@ +import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { BrandsService } from './brands.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CreateBrandDto, UpdateBrandDto } from './dto/brand.dto'; +import { IsArray, IsString } from 'class-validator'; + +class SetBrandsBody { + @IsArray() + @IsString({ each: true }) + brandIds!: string[]; +} + +@Controller() +export class BrandsController { + constructor(private readonly brandsService: BrandsService) {} + + @AuthType('admin') + @Get('admin/brands') + list(@Query('activeOnly') activeOnly?: string) { + return this.brandsService.list(activeOnly === 'true'); + } + + @AuthType('admin') + @Post('admin/brands') + create(@Body() dto: CreateBrandDto) { + return this.brandsService.create(dto); + } + + @AuthType('admin') + @Patch('admin/brands/:id') + update(@Param('id') id: string, @Body() dto: UpdateBrandDto) { + return this.brandsService.update(id, dto); + } + + @AuthType('admin') + @Get('admin/customers/:customerId/brands') + customerBrands(@Param('customerId') customerId: string) { + return this.brandsService.listCustomerBrands(customerId); + } + + @AuthType('admin') + @Post('admin/customers/:customerId/brands') + setCustomerBrands( + @Param('customerId') customerId: string, + @Body() body: SetBrandsBody, + ) { + return this.brandsService.setCustomerBrands(customerId, body.brandIds ?? []); + } +} diff --git a/src/brands/brands.module.ts b/src/brands/brands.module.ts new file mode 100644 index 0000000..eaf5ece --- /dev/null +++ b/src/brands/brands.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BrandsController } from './brands.controller'; +import { BrandsService } from './brands.service'; + +@Module({ + controllers: [BrandsController], + providers: [BrandsService], + exports: [BrandsService], +}) +export class BrandsModule {} diff --git a/src/brands/brands.service.ts b/src/brands/brands.service.ts new file mode 100644 index 0000000..ffee88d --- /dev/null +++ b/src/brands/brands.service.ts @@ -0,0 +1,112 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateBrandDto, UpdateBrandDto } from './dto/brand.dto'; + +@Injectable() +export class BrandsService { + constructor(private readonly prisma: PrismaService) {} + + async list(activeOnly?: boolean) { + const rows = await this.prisma.brand.findMany({ + where: activeOnly ? { isActive: true } : undefined, + orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }], + include: { _count: { select: { customers: true } } }, + }); + return rows.map((b) => ({ + id: b.id, + name: b.name, + slug: b.slug ?? undefined, + description: b.description ?? undefined, + isActive: b.isActive, + displayOrder: b.displayOrder, + customersCount: b._count.customers, + createdAt: b.createdAt.toISOString(), + updatedAt: b.updatedAt.toISOString(), + })); + } + + async create(dto: CreateBrandDto) { + const brand = await this.prisma.brand.create({ + data: { + name: dto.name.trim(), + slug: dto.slug?.trim() || null, + description: dto.description?.trim() || null, + displayOrder: dto.displayOrder ?? 0, + isActive: dto.isActive ?? true, + }, + }); + return this.map(brand); + } + + async update(id: string, dto: UpdateBrandDto) { + await this.ensure(id); + const brand = await this.prisma.brand.update({ + where: { id }, + data: { + name: dto.name?.trim(), + slug: + dto.slug === undefined + ? undefined + : dto.slug?.trim() || null, + description: + dto.description === undefined + ? undefined + : dto.description?.trim() || null, + displayOrder: dto.displayOrder, + isActive: dto.isActive, + }, + }); + return this.map(brand); + } + + async setCustomerBrands(customerId: string, brandIds: string[]) { + const customer = await this.prisma.customer.findUnique({ + where: { id: customerId }, + }); + if (!customer) throw new NotFoundException('مشتری یافت نشد'); + await this.prisma.customerBrand.deleteMany({ where: { customerId } }); + if (brandIds.length) { + await this.prisma.customerBrand.createMany({ + data: brandIds.map((brandId) => ({ customerId, brandId })), + skipDuplicates: true, + }); + } + return this.listCustomerBrands(customerId); + } + + async listCustomerBrands(customerId: string) { + const rows = await this.prisma.customerBrand.findMany({ + where: { customerId }, + include: { brand: true }, + }); + return rows.map((r) => this.map(r.brand)); + } + + private async ensure(id: string) { + const row = await this.prisma.brand.findUnique({ where: { id } }); + if (!row) throw new NotFoundException('برند یافت نشد'); + return row; + } + + private map(b: { + id: string; + name: string; + slug: string | null; + description: string | null; + isActive: boolean; + displayOrder: number; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: b.id, + name: b.name, + slug: b.slug ?? undefined, + description: b.description ?? undefined, + isActive: b.isActive, + displayOrder: b.displayOrder, + createdAt: b.createdAt.toISOString(), + updatedAt: b.updatedAt.toISOString(), + }; + } +} diff --git a/src/brands/dto/brand.dto.ts b/src/brands/dto/brand.dto.ts new file mode 100644 index 0000000..81dd66a --- /dev/null +++ b/src/brands/dto/brand.dto.ts @@ -0,0 +1,62 @@ +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateBrandDto { + @IsString() + @MinLength(1) + name!: string; + + @IsOptional() + @IsString() + slug?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateBrandDto { + @IsOptional() + @IsString() + @MinLength(1) + name?: string; + + @IsOptional() + @IsString() + slug?: string | null; + + @IsOptional() + @IsString() + description?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class SetCustomerBrandsDto { + brands!: string[]; +} diff --git a/src/carts/carts.controller.ts b/src/carts/carts.controller.ts new file mode 100644 index 0000000..d85829e --- /dev/null +++ b/src/carts/carts.controller.ts @@ -0,0 +1,64 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, +} from '@nestjs/common'; +import { CartsService } from './carts.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + AddCartItemDto, + UpdateCartItemMeterageDto, + UpdateCartNotesDto, +} from './dto/cart.dto'; + +@Controller('carts') +@AuthType('customer') +export class CartsController { + constructor(private readonly cartsService: CartsService) {} + + @Get() + getCart(@CurrentUser() user: AuthPrincipal) { + return this.cartsService.getCart(user.id); + } + + @Post('items') + addItem(@CurrentUser() user: AuthPrincipal, @Body() dto: AddCartItemDto) { + return this.cartsService.addItem(user.id, dto); + } + + @Patch('items/:itemId') + updateItem( + @CurrentUser() user: AuthPrincipal, + @Param('itemId') itemId: string, + @Body() dto: UpdateCartItemMeterageDto, + ) { + return this.cartsService.updateItem(user.id, itemId, dto); + } + + @Delete('items/:itemId') + removeItem( + @CurrentUser() user: AuthPrincipal, + @Param('itemId') itemId: string, + ) { + return this.cartsService.removeItem(user.id, itemId); + } + + @Patch('notes') + updateNotes( + @CurrentUser() user: AuthPrincipal, + @Body() dto: UpdateCartNotesDto, + ) { + return this.cartsService.updateNotes(user.id, dto); + } + + @Delete() + clear(@CurrentUser() user: AuthPrincipal) { + return this.cartsService.clearCart(user.id); + } +} diff --git a/src/carts/carts.module.ts b/src/carts/carts.module.ts new file mode 100644 index 0000000..cd1cee7 --- /dev/null +++ b/src/carts/carts.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CartsController } from './carts.controller'; +import { CartsService } from './carts.service'; + +@Module({ + controllers: [CartsController], + providers: [CartsService], + exports: [CartsService], +}) +export class CartsModule {} diff --git a/src/carts/carts.service.ts b/src/carts/carts.service.ts new file mode 100644 index 0000000..3d811b2 --- /dev/null +++ b/src/carts/carts.service.ts @@ -0,0 +1,249 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { + AddCartItemDto, + UpdateCartItemMeterageDto, + UpdateCartNotesDto, +} from './dto/cart.dto'; + +@Injectable() +export class CartsService { + constructor(private readonly prisma: PrismaService) {} + + async getCart(userId: string) { + const cart = await this.ensureCart(userId); + return this.mapCart(cart); + } + + async addItem(userId: string, dto: AddCartItemDto) { + const product = await this.prisma.product.findUnique({ + where: { id: dto.productId }, + include: { + catalog: true, + company: true, + }, + }); + if (!product || !product.isActive) { + throw new NotFoundException('محصول یافت نشد'); + } + await this.validateMeterage(product, dto.meterage); + + const cart = await this.ensureCart(userId); + const existing = await this.prisma.cartItem.findUnique({ + where: { + cartId_productId: { cartId: cart.id, productId: product.id }, + }, + }); + + if (existing) { + const newMeterage = existing.meterage + dto.meterage; + await this.validateMeterage(product, newMeterage); + await this.prisma.cartItem.update({ + where: { id: existing.id }, + data: { + meterage: newMeterage, + notes: dto.notes?.trim() || existing.notes, + }, + }); + } else { + await this.prisma.cartItem.create({ + data: { + cartId: cart.id, + productId: product.id, + catalogCode: product.catalog.code, + productName: product.name, + productCode: product.code, + color: product.color, + meterage: dto.meterage, + notes: dto.notes?.trim() || null, + companyId: product.companyId, + companyName: product.company.name, + }, + }); + } + + await this.prisma.cart.update({ + where: { id: cart.id }, + data: { + companyId: product.companyId, + updatedAt: new Date(), + }, + }); + + return this.getCart(userId); + } + + async updateItem( + userId: string, + itemId: string, + dto: UpdateCartItemMeterageDto, + ) { + const cart = await this.ensureCart(userId); + const item = await this.prisma.cartItem.findFirst({ + where: { id: itemId, cartId: cart.id }, + include: { product: true }, + }); + if (!item) throw new NotFoundException('آیتم سبد یافت نشد'); + await this.validateMeterage(item.product, dto.meterage); + + await this.prisma.cartItem.update({ + where: { id: itemId }, + data: { + meterage: dto.meterage, + notes: dto.notes === undefined ? undefined : dto.notes?.trim() || null, + }, + }); + await this.prisma.cart.update({ + where: { id: cart.id }, + data: { updatedAt: new Date() }, + }); + + return this.getCart(userId); + } + + async removeItem(userId: string, itemId: string) { + const cart = await this.ensureCart(userId); + const item = await this.prisma.cartItem.findFirst({ + where: { id: itemId, cartId: cart.id }, + }); + if (!item) throw new NotFoundException('آیتم سبد یافت نشد'); + + await this.prisma.cartItem.delete({ where: { id: itemId } }); + const remaining = await this.prisma.cartItem.findMany({ + where: { cartId: cart.id }, + take: 1, + }); + await this.prisma.cart.update({ + where: { id: cart.id }, + data: { + companyId: remaining[0]?.companyId ?? null, + updatedAt: new Date(), + }, + }); + + return this.getCart(userId); + } + + async updateNotes(userId: string, dto: UpdateCartNotesDto) { + const cart = await this.ensureCart(userId); + await this.prisma.cart.update({ + where: { id: cart.id }, + data: { notes: dto.notes?.trim() || null }, + }); + return this.getCart(userId); + } + + async clearCart(userId: string) { + const cart = await this.ensureCart(userId); + await this.prisma.cartItem.deleteMany({ where: { cartId: cart.id } }); + await this.prisma.cart.update({ + where: { id: cart.id }, + data: { notes: null, companyId: null }, + }); + return this.getCart(userId); + } + + private async validateMeterage( + product: { + name: string; + minMeterage: number; + orderMultiplier: number; + rollMeters?: number | null; + }, + meterage: number, + ) { + if (meterage < product.minMeterage) { + throw new BadRequestException( + `حداقل متراژ برای ${product.name} برابر ${product.minMeterage} است`, + ); + } + const rem = Math.round((meterage / product.orderMultiplier) * 1000) / 1000; + if (Math.abs(rem - Math.round(rem)) > 1e-6) { + throw new BadRequestException( + `متراژ باید مضرب ${product.orderMultiplier} باشد`, + ); + } + + const settings = await this.prisma.systemSettings.findUnique({ + where: { id: 'default' }, + }); + const minRolls = settings?.minOrderRollsPerProduct ?? 1; + const rollMeters = product.rollMeters && product.rollMeters > 0 + ? product.rollMeters + : product.minMeterage > 0 + ? product.minMeterage + : 50; + const rolls = meterage / rollMeters; + if (rolls + 1e-9 < minRolls) { + throw new BadRequestException( + `حداقل سفارش برای هر محصول ${minRolls} طاقه است (حدود ${minRolls * rollMeters} متر)`, + ); + } + } + + private async ensureCart(userId: string) { + let cart = await this.prisma.cart.findFirst({ + where: { userId }, + include: { items: true }, + }); + if (!cart) { + cart = await this.prisma.cart.create({ + data: { userId }, + include: { items: true }, + }); + } + return cart; + } + + private mapCart(cart: { + id: string; + userId: string; + companyId: string | null; + notes: string | null; + createdAt: Date; + updatedAt: Date; + items: Array<{ + id: string; + productId: string; + catalogCode: string; + productName: string; + productCode: string; + color: string; + meterage: number; + notes: string | null; + companyId: string; + companyName: string; + }>; + }) { + const companyNames = [ + ...new Set(cart.items.map((item) => item.companyName).filter(Boolean)), + ]; + return { + id: cart.id, + userId: cart.userId, + companyId: cart.companyId ?? cart.items[0]?.companyId, + companyName: companyNames.join('، '), + notes: cart.notes ?? undefined, + items: cart.items.map((item) => ({ + id: item.id, + productId: item.productId, + catalogCode: item.catalogCode, + productName: item.productName, + productCode: item.productCode, + color: item.color, + meterage: item.meterage, + notes: item.notes ?? undefined, + companyId: item.companyId, + companyName: item.companyName, + })), + totalMeterage: cart.items.reduce((s, i) => s + i.meterage, 0), + itemCount: cart.items.length, + createdAt: cart.createdAt.toISOString(), + updatedAt: cart.updatedAt.toISOString(), + }; + } +} diff --git a/src/carts/dto/cart.dto.ts b/src/carts/dto/cart.dto.ts new file mode 100644 index 0000000..c6be0ac --- /dev/null +++ b/src/carts/dto/cart.dto.ts @@ -0,0 +1,33 @@ +import { IsNumber, IsOptional, IsString, Min } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class AddCartItemDto { + @IsString() + productId!: string; + + @Type(() => Number) + @IsNumber() + @Min(0.001) + meterage!: number; + + @IsOptional() + @IsString() + notes?: string; +} + +export class UpdateCartItemMeterageDto { + @Type(() => Number) + @IsNumber() + @Min(0.001) + meterage!: number; + + @IsOptional() + @IsString() + notes?: string; +} + +export class UpdateCartNotesDto { + @IsOptional() + @IsString() + notes?: string; +} diff --git a/src/cash-payments/cash-payments.controller.ts b/src/cash-payments/cash-payments.controller.ts new file mode 100644 index 0000000..648dafb --- /dev/null +++ b/src/cash-payments/cash-payments.controller.ts @@ -0,0 +1,51 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { CashPaymentStatus } from '@prisma/client'; +import { CashPaymentsService } from './cash-payments.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + SubmitCashReceiptDto, + UpdateCashPaymentStatusDto, +} from './dto/cash-payment.dto'; + +@Controller() +export class CashPaymentsController { + constructor(private readonly cashPaymentsService: CashPaymentsService) {} + + @AuthType('customer') + @Post('orders/:orderId/cash-receipt') + submitReceipt( + @CurrentUser() user: AuthPrincipal, + @Param('orderId') orderId: string, + @Body() dto: SubmitCashReceiptDto, + ) { + return this.cashPaymentsService.submitReceipt(user.id, orderId, dto); + } + + @AuthType('admin') + @Roles('admin', 'finance') + @Get('admin/cash-payments') + findAll(@Query('status') status?: CashPaymentStatus) { + return this.cashPaymentsService.findAll(status); + } + + @AuthType('admin') + @Roles('admin', 'finance') + @Patch('admin/cash-payments/:id/status') + updateStatus( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: UpdateCashPaymentStatusDto, + ) { + return this.cashPaymentsService.updateStatus(id, user.id, dto); + } +} diff --git a/src/cash-payments/cash-payments.module.ts b/src/cash-payments/cash-payments.module.ts new file mode 100644 index 0000000..9e20bf5 --- /dev/null +++ b/src/cash-payments/cash-payments.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CashPaymentsController } from './cash-payments.controller'; +import { CashPaymentsService } from './cash-payments.service'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [NotificationsModule], + controllers: [CashPaymentsController], + providers: [CashPaymentsService], + exports: [CashPaymentsService], +}) +export class CashPaymentsModule {} diff --git a/src/cash-payments/cash-payments.service.ts b/src/cash-payments/cash-payments.service.ts new file mode 100644 index 0000000..c6a3ae5 --- /dev/null +++ b/src/cash-payments/cash-payments.service.ts @@ -0,0 +1,271 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CashPaymentStatus, OrderStatus, SettlementType } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + SubmitCashReceiptDto, + UpdateCashPaymentStatusDto, +} from './dto/cash-payment.dto'; +import { joinFullName } from '../common/utils/name'; +import { NotificationsService } from '../notifications/notifications.service'; + +/** بعد از تأیید پرداخت یا ارسال، رسید جدید نباید وضعیت را به عقب برگرداند. */ +const RECEIPT_SUBMITTABLE_STATUSES: OrderStatus[] = [ + OrderStatus.cash_in_progress, + OrderStatus.cash_invoice_issued, + OrderStatus.awaiting_cash_payment, + OrderStatus.cash_payment_submitted, + OrderStatus.awaiting_cash_confirmation, +]; + +@Injectable() +export class CashPaymentsService { + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} + + async submitReceipt( + userId: string, + orderId: string, + dto: SubmitCashReceiptDto, + ) { + const order = await this.prisma.order.findUnique({ + where: { id: orderId }, + include: { cashReceipt: true, items: true }, + }); + if (!order) throw new NotFoundException('سفارش یافت نشد'); + if (order.userId !== userId) { + throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); + } + if (order.settlementType !== SettlementType.cash) { + throw new BadRequestException('این سفارش تسویه نقدی نیست'); + } + if (!RECEIPT_SUBMITTABLE_STATUSES.includes(order.status)) { + throw new BadRequestException( + 'در وضعیت فعلی سفارش امکان ثبت رسید وجود ندارد', + ); + } + if ( + order.cashInvoiceExpiresAt && + order.cashInvoiceExpiresAt.getTime() < Date.now() + ) { + throw new BadRequestException( + 'مهلت پرداخت فاکتور نقدی به پایان رسیده است', + ); + } + + const companyIds = [ + ...new Set( + [ + order.companyId, + ...order.items.map((item) => item.companyId).filter(Boolean), + ].filter(Boolean), + ), + ]; + const account = await this.prisma.bankAccount.findFirst({ + where: { + id: dto.destinationAccountId, + companyId: { in: companyIds }, + isActive: true, + }, + }); + if (!account) { + throw new BadRequestException('حساب مقصد نامعتبر است'); + } + + const receipt = await this.prisma.$transaction(async (tx) => { + const data = { + amount: dto.amount, + paidAmount: dto.paidAmount ?? dto.amount, + transferDate: new Date(dto.transferDate), + transferTime: dto.transferTime.trim(), + trackingNumber: dto.trackingNumber.trim(), + destinationAccountId: dto.destinationAccountId, + receiptImage: dto.receiptImage || null, + notes: dto.notes?.trim() || null, + expiresAt: order.cashInvoiceExpiresAt, + status: CashPaymentStatus.pending_review, + submittedAt: new Date(), + }; + + const saved = order.cashReceipt + ? await tx.cashPaymentReceipt.update({ + where: { orderId }, + data, + }) + : await tx.cashPaymentReceipt.create({ + data: { orderId, ...data }, + }); + + await tx.order.update({ + where: { id: orderId }, + data: { status: OrderStatus.awaiting_cash_confirmation }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId, + fromStatus: order.status, + toStatus: OrderStatus.awaiting_cash_confirmation, + changedBy: 'customer', + note: 'رسید پرداخت نقدی ارسال شد', + }, + }); + + return saved; + }); + + this.notifications.dispatchAdmin({ + title: 'رسید پرداخت نقدی ثبت شد', + body: `مشتری برای سفارش ${order.orderNumber} رسید به مبلغ ${Math.round( + dto.amount, + ).toLocaleString('fa-IR')} تومان ثبت کرد.`, + link: `/orders/${orderId}`, + }); + + return this.mapReceipt(receipt, order.orderNumber, ''); + } + + async findAll(status?: CashPaymentStatus) { + const receipts = await this.prisma.cashPaymentReceipt.findMany({ + where: status ? { status } : undefined, + orderBy: { createdAt: 'desc' }, + include: { + order: { + include: { user: true }, + }, + }, + }); + return receipts.map((r) => + this.mapReceipt( + r, + r.order.orderNumber, + joinFullName(r.order.user.firstName, r.order.user.lastName), + ), + ); + } + + async updateStatus( + id: string, + adminId: string, + dto: UpdateCashPaymentStatusDto, + ) { + const receipt = await this.prisma.cashPaymentReceipt.findUnique({ + where: { id }, + include: { order: { include: { user: true } } }, + }); + if (!receipt) throw new NotFoundException('رسید نقدی یافت نشد'); + + const updated = await this.prisma.$transaction(async (tx) => { + const result = await tx.cashPaymentReceipt.update({ + where: { id }, + data: { + status: dto.status, + notes: dto.note + ? [receipt.notes, dto.note].filter(Boolean).join('\n') + : receipt.notes, + }, + include: { order: { include: { user: true } } }, + }); + + // رد رسید باید مشتری را به مرحله پرداخت برگرداند تا بتواند رسید تازه بفرستد. + let nextStatus: OrderStatus | null = null; + if (dto.status === CashPaymentStatus.approved) { + nextStatus = OrderStatus.cash_confirmed; + } else if (dto.status === CashPaymentStatus.rejected) { + nextStatus = OrderStatus.awaiting_cash_payment; + } else if (dto.status === CashPaymentStatus.expired) { + nextStatus = OrderStatus.expired; + } + + if (nextStatus && nextStatus !== receipt.order.status) { + await tx.order.update({ + where: { id: receipt.orderId }, + data: { status: nextStatus }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: receipt.orderId, + fromStatus: receipt.order.status, + toStatus: nextStatus, + changedBy: `admin:${adminId}`, + note: dto.note || `وضعیت پرداخت نقدی: ${dto.status}`, + }, + }); + } + + return result; + }); + + if (dto.status === CashPaymentStatus.approved) { + this.notifications.dispatchOrderEvent( + receipt.orderId, + 'receipt_approved', + ); + } else if (dto.status === CashPaymentStatus.rejected) { + this.notifications.dispatchOrderEvent( + receipt.orderId, + 'receipt_rejected', + { note: dto.note ?? null }, + ); + } else if (dto.status === CashPaymentStatus.expired) { + this.notifications.dispatchOrderEvent( + receipt.orderId, + 'cash_invoice_expired', + ); + } + + return this.mapReceipt( + updated, + updated.order.orderNumber, + joinFullName(updated.order.user.firstName, updated.order.user.lastName), + ); + } + + private mapReceipt( + r: { + id: string; + orderId: string; + amount: number; + paidAmount: number | null; + transferDate: Date | null; + transferTime: string | null; + trackingNumber: string | null; + destinationAccountId: string | null; + receiptImage: string | null; + notes: string | null; + expiresAt: Date | null; + status: CashPaymentStatus; + submittedAt: Date | null; + createdAt: Date; + updatedAt: Date; + }, + orderNumber: string, + customerName: string, + ) { + return { + id: r.id, + orderId: r.orderId, + orderNumber, + customerName, + amount: r.amount, + paidAmount: r.paidAmount, + transferDate: r.transferDate?.toISOString() ?? null, + transferTime: r.transferTime ?? undefined, + trackingNumber: r.trackingNumber ?? '', + trackingCode: r.trackingNumber ?? '', + destinationAccountId: r.destinationAccountId ?? undefined, + receiptImage: r.receiptImage ?? undefined, + notes: r.notes ?? undefined, + expiresAt: r.expiresAt?.toISOString() ?? '', + status: r.status, + submittedAt: r.submittedAt?.toISOString(), + createdAt: r.createdAt.toISOString(), + updatedAt: r.updatedAt.toISOString(), + }; + } +} diff --git a/src/cash-payments/dto/cash-payment.dto.ts b/src/cash-payments/dto/cash-payment.dto.ts new file mode 100644 index 0000000..a190968 --- /dev/null +++ b/src/cash-payments/dto/cash-payment.dto.ts @@ -0,0 +1,55 @@ +import { + IsDateString, + IsEnum, + IsNumber, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { CashPaymentStatus } from '@prisma/client'; + +export class SubmitCashReceiptDto { + @Type(() => Number) + @IsNumber() + @Min(1) + amount!: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + paidAmount?: number; + + @IsDateString() + transferDate!: string; + + @IsString() + @MinLength(1) + transferTime!: string; + + @IsString() + @MinLength(1) + trackingNumber!: string; + + @IsString() + destinationAccountId!: string; + + @IsOptional() + @IsString() + receiptImage?: string; + + @IsOptional() + @IsString() + notes?: string; +} + +export class UpdateCashPaymentStatusDto { + @IsEnum(CashPaymentStatus) + status!: CashPaymentStatus; + + @IsOptional() + @IsString() + note?: string; +} diff --git a/src/catalog-requests/catalog-requests.controller.ts b/src/catalog-requests/catalog-requests.controller.ts new file mode 100644 index 0000000..27eedd6 --- /dev/null +++ b/src/catalog-requests/catalog-requests.controller.ts @@ -0,0 +1,82 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { CatalogRequestStatus } from '@prisma/client'; +import { CatalogRequestsService } from './catalog-requests.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + CreateCatalogRequestDto, + UpdateCatalogItemHolderDto, + UpdateCatalogRequestStatusDto, +} from './dto/catalog-request.dto'; + +@Controller() +export class CatalogRequestsController { + constructor(private readonly service: CatalogRequestsService) {} + + @AuthType('admin') + @Roles('admin', 'sales', 'warehouse') + @Get('admin/catalog-requests') + adminList(@Query('status') status?: CatalogRequestStatus) { + return this.service.listAdmin(status); + } + + @AuthType('admin') + @Roles('admin', 'sales', 'warehouse') + @Get('admin/catalog-requests/:id') + adminGet(@Param('id') id: string) { + return this.service.get(id); + } + + @AuthType('admin') + @Roles('admin', 'sales', 'warehouse') + @Patch('admin/catalog-requests/:id/status') + adminStatus( + @Param('id') id: string, + @Body() dto: UpdateCatalogRequestStatusDto, + ) { + return this.service.updateStatus(id, dto); + } + + @AuthType('admin') + @Roles('admin', 'sales', 'warehouse') + @Patch('admin/catalog-request-items/:id/holder') + adminHolder( + @Param('id') id: string, + @Body() dto: UpdateCatalogItemHolderDto, + ) { + return this.service.updateItemHolder(id, dto); + } + + @AuthType('customer') + @Get('catalog-requests') + mine(@CurrentUser() user: AuthPrincipal) { + return this.service.listMine(user.id); + } + + @AuthType('customer') + @Post('catalog-requests') + create( + @CurrentUser() user: AuthPrincipal, + @Body() dto: CreateCatalogRequestDto, + ) { + return this.service.create(user.id, dto); + } + + @AuthType('customer') + @Get('catalog-requests/:id') + async mineOne(@CurrentUser() user: AuthPrincipal, @Param('id') id: string) { + const row = await this.service.get(id); + if (row.customerId !== user.id) throw new ForbiddenException(); + return row; + } +} diff --git a/src/catalog-requests/catalog-requests.module.ts b/src/catalog-requests/catalog-requests.module.ts new file mode 100644 index 0000000..466bcb9 --- /dev/null +++ b/src/catalog-requests/catalog-requests.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CatalogRequestsController } from './catalog-requests.controller'; +import { CatalogRequestsService } from './catalog-requests.service'; + +@Module({ + controllers: [CatalogRequestsController], + providers: [CatalogRequestsService], + exports: [CatalogRequestsService], +}) +export class CatalogRequestsModule {} diff --git a/src/catalog-requests/catalog-requests.service.ts b/src/catalog-requests/catalog-requests.service.ts new file mode 100644 index 0000000..df2296e --- /dev/null +++ b/src/catalog-requests/catalog-requests.service.ts @@ -0,0 +1,264 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { + CatalogItemHolderType, + CatalogRequestStatus, +} from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + CreateCatalogRequestDto, + UpdateCatalogRequestStatusDto, + UpdateCatalogItemHolderDto, +} from './dto/catalog-request.dto'; + +const STATUS_FLOW: CatalogRequestStatus[] = [ + 'new', + 'approved', + 'ready_to_ship', + 'delivered', + 'returned', + 'cancelled', +]; + +@Injectable() +export class CatalogRequestsService { + constructor(private readonly prisma: PrismaService) {} + + async listAdmin(status?: CatalogRequestStatus) { + const rows = await this.prisma.catalogRequest.findMany({ + where: status ? { status } : undefined, + orderBy: { createdAt: 'desc' }, + include: { + customer: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, + items: { + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: { orderBy: { movedAt: 'desc' }, take: 5 }, + }, + }, + }, + }); + return rows.map((r) => this.map(r)); + } + + async listMine(customerId: string) { + const rows = await this.prisma.catalogRequest.findMany({ + where: { customerId }, + orderBy: { createdAt: 'desc' }, + include: { + customer: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, + items: { + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: { orderBy: { movedAt: 'desc' }, take: 5 }, + }, + }, + }, + }); + return rows.map((r) => this.map(r)); + } + + async get(id: string) { + const row = await this.prisma.catalogRequest.findUnique({ + where: { id }, + include: { + customer: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, + items: { + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: { orderBy: { movedAt: 'desc' } }, + }, + }, + }, + }); + if (!row) throw new NotFoundException('درخواست کالیته یافت نشد'); + return this.map(row); + } + + async create(customerId: string, dto: CreateCatalogRequestDto) { + const items = (dto.items ?? []).filter((i) => i.productId && i.quantity > 0); + if (!items.length) { + throw new BadRequestException('حداقل یک قلم با تعداد معتبر لازم است'); + } + const productIds = [...new Set(items.map((i) => i.productId))]; + const products = await this.prisma.product.findMany({ + where: { id: { in: productIds } }, + select: { id: true }, + }); + if (products.length !== productIds.length) { + throw new BadRequestException('یکی از محصولات معتبر نیست'); + } + + const row = await this.prisma.catalogRequest.create({ + data: { + customerId, + note: dto.note?.trim() || null, + status: 'new', + items: { + create: items.map((i) => ({ + productId: i.productId, + quantity: Math.max(1, Math.floor(i.quantity)), + holderType: 'warehouse', + })), + }, + }, + include: { + customer: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, + items: { + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: true, + }, + }, + }, + }); + return this.map(row); + } + + async updateStatus(id: string, dto: UpdateCatalogRequestStatusDto) { + await this.ensure(id); + if (!STATUS_FLOW.includes(dto.status)) { + throw new BadRequestException('وضعیت نامعتبر است'); + } + const row = await this.prisma.catalogRequest.update({ + where: { id }, + data: { status: dto.status }, + include: { + customer: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, + items: { + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: { orderBy: { movedAt: 'desc' }, take: 5 }, + }, + }, + }, + }); + + if (dto.status === 'delivered') { + await this.prisma.catalogRequestItem.updateMany({ + where: { requestId: id }, + data: { holderType: 'customer' }, + }); + } + + return this.map(row); + } + + async updateItemHolder(itemId: string, dto: UpdateCatalogItemHolderDto) { + const item = await this.prisma.catalogRequestItem.findUnique({ + where: { id: itemId }, + }); + if (!item) throw new NotFoundException('قلم کالیته یافت نشد'); + + const fromHolder = `${item.holderType}:${item.holderCustomerId ?? 'warehouse'}`; + const toHolder = `${dto.holderType}:${dto.holderCustomerId ?? 'warehouse'}`; + + const updated = await this.prisma.catalogRequestItem.update({ + where: { id: itemId }, + data: { + holderType: dto.holderType, + holderCustomerId: dto.holderCustomerId ?? null, + }, + include: { + product: { select: { id: true, name: true, code: true, color: true } }, + movements: { orderBy: { movedAt: 'desc' } }, + }, + }); + + await this.prisma.catalogItemMovement.create({ + data: { + itemId, + fromHolder, + toHolder, + note: dto.note?.trim() || null, + }, + }); + + return { + id: updated.id, + productId: updated.productId, + productName: updated.product.name, + productCode: updated.product.code, + quantity: updated.quantity, + holderType: updated.holderType, + holderCustomerId: updated.holderCustomerId, + }; + } + + private async ensure(id: string) { + const row = await this.prisma.catalogRequest.findUnique({ where: { id } }); + if (!row) throw new NotFoundException('درخواست کالیته یافت نشد'); + return row; + } + + private map(row: { + id: string; + customerId: string; + status: CatalogRequestStatus; + note: string | null; + createdAt: Date; + updatedAt: Date; + customer: { + id: string; + firstName: string; + lastName: string; + phone: string; + }; + items: Array<{ + id: string; + productId: string; + quantity: number; + holderType: CatalogItemHolderType; + holderCustomerId: string | null; + product: { id: string; name: string; code: string; color: string }; + movements: Array<{ + id: string; + fromHolder: string; + toHolder: string; + note: string | null; + movedAt: Date; + }>; + }>; + }) { + return { + id: row.id, + customerId: row.customerId, + customerName: `${row.customer.firstName} ${row.customer.lastName}`.trim(), + customerPhone: row.customer.phone, + status: row.status, + note: row.note ?? undefined, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + items: row.items.map((i) => ({ + id: i.id, + productId: i.productId, + productName: i.product.name, + productCode: i.product.code, + color: i.product.color, + quantity: i.quantity, + holderType: i.holderType, + holderCustomerId: i.holderCustomerId, + movements: i.movements.map((m) => ({ + id: m.id, + fromHolder: m.fromHolder, + toHolder: m.toHolder, + note: m.note ?? undefined, + movedAt: m.movedAt.toISOString(), + })), + })), + }; + } +} diff --git a/src/catalog-requests/dto/catalog-request.dto.ts b/src/catalog-requests/dto/catalog-request.dto.ts new file mode 100644 index 0000000..ab892c9 --- /dev/null +++ b/src/catalog-requests/dto/catalog-request.dto.ts @@ -0,0 +1,57 @@ +import { + ArrayMinSize, + IsArray, + IsEnum, + IsInt, + IsOptional, + IsString, + Min, + MinLength, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { + CatalogItemHolderType, + CatalogRequestStatus, +} from '@prisma/client'; + +export class CatalogRequestItemInputDto { + @IsString() + @MinLength(1) + productId!: string; + + @Type(() => Number) + @IsInt() + @Min(1) + quantity!: number; +} + +export class CreateCatalogRequestDto { + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CatalogRequestItemInputDto) + items!: CatalogRequestItemInputDto[]; + + @IsOptional() + @IsString() + note?: string; +} + +export class UpdateCatalogRequestStatusDto { + @IsEnum(CatalogRequestStatus) + status!: CatalogRequestStatus; +} + +export class UpdateCatalogItemHolderDto { + @IsEnum(CatalogItemHolderType) + holderType!: CatalogItemHolderType; + + @IsOptional() + @IsString() + holderCustomerId?: string | null; + + @IsOptional() + @IsString() + note?: string; +} diff --git a/src/catalog/category-defs.ts b/src/catalog/category-defs.ts new file mode 100644 index 0000000..aa42fdc --- /dev/null +++ b/src/catalog/category-defs.ts @@ -0,0 +1,58 @@ +import type { PrismaService } from '../prisma/prisma.service' + +export const USAGE_CATEGORIES = [ + { id: 'trousers', title: 'پارچه‌های شلواری', subtitle: 'مشاهده محصولات' }, + { id: 'suit', title: 'پارچه‌های کت‌شلواری', subtitle: 'مشاهده محصولات' }, + { id: 'shirt', title: 'پارچه‌های پیراهنی', subtitle: 'مشاهده محصولات' }, +] as const + +export const SEASON_CATEGORIES = [ + { + id: 'summer', + title: 'تابستانه', + description: 'سبک، خنک و مناسب فصل گرم', + }, + { + id: 'winter', + title: 'زمستانه', + description: 'گرم و مناسب فصل سرد', + }, + { + id: 'all_season', + title: 'چهارفصل', + description: 'مناسب استفاده در بیشتر فصل‌های سال', + }, +] as const + +export const CATEGORY_IMAGE_KEYS = [ + ...USAGE_CATEGORIES.map((c) => c.id), + ...SEASON_CATEGORIES.map((c) => c.id), +] as const + +export type CategoryImageKey = (typeof CATEGORY_IMAGE_KEYS)[number] + +export function asImageMap(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {} + const out: Record = {} + for (const [key, image] of Object.entries(value as Record)) { + if (typeof image === 'string' && image.trim()) out[key] = image.trim() + } + return out +} + +export async function loadCatalogCategories(prisma: PrismaService) { + const settings = await prisma.systemSettings.findUnique({ + where: { id: 'default' }, + }) + const images = asImageMap(settings?.categoryImagesJson) + return { + usage: USAGE_CATEGORIES.map((item) => ({ + ...item, + imageUrl: images[item.id] || undefined, + })), + seasons: SEASON_CATEGORIES.map((item) => ({ + ...item, + imageUrl: images[item.id] || undefined, + })), + } +} diff --git a/src/catalogs/catalogs.controller.ts b/src/catalogs/catalogs.controller.ts new file mode 100644 index 0000000..c9b3860 --- /dev/null +++ b/src/catalogs/catalogs.controller.ts @@ -0,0 +1,57 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { CatalogsService } from './catalogs.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { + CreateCatalogDto, + SetCatalogActiveDto, + UpdateCatalogDto, +} from './dto/catalog.dto'; + +@Controller('catalogs') +export class CatalogsController { + constructor(private readonly catalogsService: CatalogsService) {} + + @AuthType('any') + @Get() + findAll( + @Query('companyId') companyId?: string, + @Query('activeOnly') activeOnly?: string, + ) { + return this.catalogsService.findAll( + companyId, + activeOnly === 'true' || activeOnly === '1', + ); + } + + @AuthType('any') + @Get(':id') + findOne(@Param('id') id: string) { + return this.catalogsService.findOne(id); + } + + @AuthType('admin') + @Post() + create(@Body() dto: CreateCatalogDto) { + return this.catalogsService.create(dto); + } + + @AuthType('admin') + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateCatalogDto) { + return this.catalogsService.update(id, dto); + } + + @AuthType('admin') + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetCatalogActiveDto) { + return this.catalogsService.setActive(id, dto.isActive); + } +} diff --git a/src/catalogs/catalogs.module.ts b/src/catalogs/catalogs.module.ts new file mode 100644 index 0000000..57ca02c --- /dev/null +++ b/src/catalogs/catalogs.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CatalogsController } from './catalogs.controller'; +import { CatalogsService } from './catalogs.service'; + +@Module({ + controllers: [CatalogsController], + providers: [CatalogsService], + exports: [CatalogsService], +}) +export class CatalogsModule {} diff --git a/src/catalogs/catalogs.service.ts b/src/catalogs/catalogs.service.ts new file mode 100644 index 0000000..26f87ce --- /dev/null +++ b/src/catalogs/catalogs.service.ts @@ -0,0 +1,152 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateCatalogDto, UpdateCatalogDto } from './dto/catalog.dto'; + +@Injectable() +export class CatalogsService { + constructor(private readonly prisma: PrismaService) {} + + async findAll(companyId?: string, activeOnly = false) { + const catalogs = await this.prisma.catalog.findMany({ + where: { + ...(companyId ? { companyId } : {}), + ...(activeOnly ? { isActive: true } : {}), + }, + orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }], + include: { + company: { select: { name: true } }, + _count: { select: { products: true } }, + }, + }); + return catalogs.map((c) => this.map(c)); + } + + async findOne(id: string) { + const catalog = await this.prisma.catalog.findUnique({ + where: { id }, + include: { + company: { select: { name: true } }, + _count: { select: { products: true } }, + }, + }); + if (!catalog) throw new NotFoundException('کاتالوگ یافت نشد'); + return this.map(catalog); + } + + async create(dto: CreateCatalogDto) { + const company = await this.prisma.company.findUnique({ + where: { id: dto.companyId }, + }); + if (!company) throw new NotFoundException('شرکت یافت نشد'); + + try { + const catalog = await this.prisma.catalog.create({ + data: { + companyId: dto.companyId, + name: dto.name.trim(), + code: dto.code.trim(), + description: dto.description?.trim() || null, + displayOrder: dto.displayOrder ?? 0, + isActive: dto.isActive ?? true, + }, + include: { + company: { select: { name: true } }, + _count: { select: { products: true } }, + }, + }); + return this.map(catalog); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('کد کاتالوگ در این شرکت تکراری است'); + } + throw e; + } + } + + async update(id: string, dto: UpdateCatalogDto) { + await this.ensure(id); + try { + const catalog = await this.prisma.catalog.update({ + where: { id }, + data: { + name: dto.name?.trim(), + code: dto.code?.trim(), + description: + dto.description === undefined + ? undefined + : dto.description?.trim() || null, + displayOrder: dto.displayOrder, + isActive: dto.isActive, + }, + include: { + company: { select: { name: true } }, + _count: { select: { products: true } }, + }, + }); + return this.map(catalog); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('کد کاتالوگ در این شرکت تکراری است'); + } + throw e; + } + } + + async setActive(id: string, isActive: boolean) { + await this.ensure(id); + const catalog = await this.prisma.catalog.update({ + where: { id }, + data: { isActive }, + include: { + company: { select: { name: true } }, + _count: { select: { products: true } }, + }, + }); + return this.map(catalog); + } + + private async ensure(id: string) { + const catalog = await this.prisma.catalog.findUnique({ where: { id } }); + if (!catalog) throw new NotFoundException('کاتالوگ یافت نشد'); + return catalog; + } + + private map(catalog: { + id: string; + companyId: string; + name: string; + code: string; + description: string | null; + displayOrder: number; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + company: { name: string }; + _count: { products: number }; + }) { + return { + id: catalog.id, + companyId: catalog.companyId, + companyName: catalog.company.name, + name: catalog.name, + code: catalog.code, + description: catalog.description ?? undefined, + displayOrder: catalog.displayOrder, + isActive: catalog.isActive, + productCount: catalog._count.products, + createdAt: catalog.createdAt.toISOString(), + updatedAt: catalog.updatedAt.toISOString(), + }; + } +} diff --git a/src/catalogs/dto/catalog.dto.ts b/src/catalogs/dto/catalog.dto.ts new file mode 100644 index 0000000..6ee16a8 --- /dev/null +++ b/src/catalogs/dto/catalog.dto.ts @@ -0,0 +1,64 @@ +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateCatalogDto { + @IsString() + companyId!: string; + + @IsString() + @MinLength(1) + name!: string; + + @IsString() + @MinLength(1) + code!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateCatalogDto { + @IsOptional() + @IsString() + @MinLength(1) + name?: string; + + @IsOptional() + @IsString() + @MinLength(1) + code?: string; + + @IsOptional() + @IsString() + description?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class SetCatalogActiveDto { + @IsBoolean() + isActive!: boolean; +} diff --git a/src/checks/checks.controller.ts b/src/checks/checks.controller.ts new file mode 100644 index 0000000..9f57d07 --- /dev/null +++ b/src/checks/checks.controller.ts @@ -0,0 +1,32 @@ +import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; +import { ChecksService } from './checks.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { UpdateCheckStatusDto } from './dto/check.dto'; + +@Controller() +@AuthType('admin') +@Roles('admin', 'finance') +export class ChecksController { + constructor(private readonly checksService: ChecksService) {} + + @Get('admin/checks') + findAll(@Query('status') status?: string) { + return this.checksService.findAll(status); + } + + @Get('admin/orders/:orderId/checks') + findByOrder(@Param('orderId') orderId: string) { + return this.checksService.findByOrder(orderId); + } + + @Patch('admin/checks/:id/status') + updateStatus( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: UpdateCheckStatusDto, + ) { + return this.checksService.updateStatus(id, user.id, dto); + } +} diff --git a/src/checks/checks.module.ts b/src/checks/checks.module.ts new file mode 100644 index 0000000..40af8ad --- /dev/null +++ b/src/checks/checks.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ChecksController } from './checks.controller'; +import { ChecksService } from './checks.service'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [NotificationsModule], + controllers: [ChecksController], + providers: [ChecksService], + exports: [ChecksService], +}) +export class ChecksModule {} diff --git a/src/checks/checks.service.ts b/src/checks/checks.service.ts new file mode 100644 index 0000000..feaec04 --- /dev/null +++ b/src/checks/checks.service.ts @@ -0,0 +1,164 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CheckStatus, OrderStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { UpdateCheckStatusDto } from './dto/check.dto'; +import { NotificationsService } from '../notifications/notifications.service'; +import { joinFullName } from '../common/utils/name'; + +@Injectable() +export class ChecksService { + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} + + async findByOrder(orderId: string) { + const order = await this.prisma.order.findUnique({ + where: { id: orderId }, + }); + if (!order) throw new NotFoundException('سفارش یافت نشد'); + + const checks = await this.prisma.check.findMany({ + where: { orderId }, + orderBy: { row: 'asc' }, + }); + return checks.map((c) => this.map(c)); + } + + async findAll(status?: string) { + const where = + status && Object.values(CheckStatus).includes(status as CheckStatus) + ? { status: status as CheckStatus } + : {}; + + const checks = await this.prisma.check.findMany({ + where, + include: { + order: { + include: { user: true }, + }, + }, + orderBy: [{ dueDate: 'asc' }, { createdAt: 'desc' }], + }); + + return checks.map((c) => ({ + ...this.map(c), + orderNumber: c.order.orderNumber, + customerName: joinFullName(c.order.user.firstName, c.order.user.lastName), + orderStatus: c.order.status, + settlementType: c.order.settlementType, + })); + } + + async updateStatus(id: string, adminId: string, dto: UpdateCheckStatusDto) { + const check = await this.prisma.check.findUnique({ + where: { id }, + include: { order: { include: { checks: true } } }, + }); + if (!check) throw new NotFoundException('چک یافت نشد'); + + let notifyStatus: OrderStatus | null = null; + const updated = await this.prisma.$transaction(async (tx) => { + const result = await tx.check.update({ + where: { id }, + data: { + status: dto.status, + notes: dto.note + ? [check.notes, dto.note].filter(Boolean).join('\n') + : check.notes, + }, + }); + + const siblings = await tx.check.findMany({ + where: { orderId: check.orderId }, + }); + const allApproved = siblings.every( + (c) => c.status === CheckStatus.approved, + ); + const anyRejected = siblings.some( + (c) => c.status === CheckStatus.rejected, + ); + + // رد چک باید قابل اصلاح بماند؛ check_rejected راه برگشتی برای مشتری ندارد. + let nextStatus: OrderStatus | null = null; + if (anyRejected) { + nextStatus = OrderStatus.check_needs_fix; + } else if (allApproved) { + nextStatus = OrderStatus.checks_approved; + } + + if (nextStatus && nextStatus !== check.order.status) { + await tx.order.update({ + where: { id: check.orderId }, + data: { status: nextStatus }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: check.orderId, + fromStatus: check.order.status, + toStatus: nextStatus, + changedBy: `admin:${adminId}`, + note: dto.note || `وضعیت چک به ${dto.status} تغییر کرد`, + }, + }); + notifyStatus = nextStatus; + } + + return result; + }); + + if (notifyStatus === OrderStatus.checks_approved) { + this.notifications.dispatchOrderEvent(check.orderId, 'checks_approved'); + } else if (notifyStatus === OrderStatus.check_needs_fix) { + this.notifications.dispatchOrderEvent(check.orderId, 'checks_need_fix', { + note: dto.note ?? null, + }); + } + + return this.map(updated); + } + + private map(c: { + id: string; + orderId: string; + row: number; + amount: number; + dueDate: Date; + sayadId: string | null; + checkNumber: string | null; + ownerNationalId: string | null; + ownerFirstName: string | null; + ownerLastName: string | null; + bankName: string | null; + branchName: string | null; + frontImage: string | null; + backImage: string | null; + notes: string | null; + mode: string; + status: CheckStatus; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: c.id, + orderId: c.orderId, + row: c.row, + amount: c.amount, + dueDate: c.dueDate.toISOString(), + sayadId: c.sayadId ?? '', + checkNumber: c.checkNumber ?? undefined, + ownerNationalId: c.ownerNationalId ?? undefined, + ownerFirstName: c.ownerFirstName ?? undefined, + ownerLastName: c.ownerLastName ?? undefined, + bankName: c.bankName ?? '', + branchName: c.branchName ?? undefined, + frontImage: c.frontImage ?? undefined, + backImage: c.backImage ?? undefined, + notes: c.notes ?? undefined, + mode: c.mode, + status: c.status, + createdAt: c.createdAt.toISOString(), + updatedAt: c.updatedAt.toISOString(), + }; + } +} diff --git a/src/checks/dto/check.dto.ts b/src/checks/dto/check.dto.ts new file mode 100644 index 0000000..4abb2e6 --- /dev/null +++ b/src/checks/dto/check.dto.ts @@ -0,0 +1,11 @@ +import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { CheckStatus } from '@prisma/client'; + +export class UpdateCheckStatusDto { + @IsEnum(CheckStatus) + status!: CheckStatus; + + @IsOptional() + @IsString() + note?: string; +} diff --git a/src/common/decorators/auth.decorators.ts b/src/common/decorators/auth.decorators.ts new file mode 100644 index 0000000..8de9714 --- /dev/null +++ b/src/common/decorators/auth.decorators.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); + +export const AUTH_TYPE_KEY = 'authType'; +export const AuthType = (type: 'customer' | 'admin' | 'any') => + SetMetadata(AUTH_TYPE_KEY, type); diff --git a/src/common/decorators/current-user.decorator.ts b/src/common/decorators/current-user.decorator.ts new file mode 100644 index 0000000..5e87bbe --- /dev/null +++ b/src/common/decorators/current-user.decorator.ts @@ -0,0 +1,15 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export interface AuthPrincipal { + id: string; + type: 'customer' | 'admin'; + role?: string; + phone?: string; +} + +export const CurrentUser = createParamDecorator( + (_data: unknown, ctx: ExecutionContext): AuthPrincipal => { + const request = ctx.switchToHttp().getRequest<{ user: AuthPrincipal }>(); + return request.user; + }, +); diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..bb52dfa --- /dev/null +++ b/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,78 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, + ForbiddenException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { + AUTH_TYPE_KEY, + IS_PUBLIC_KEY, + ROLES_KEY, +} from '../decorators/auth.decorators'; +import { AuthPrincipal } from '../decorators/current-user.decorator'; +import { resolveJwtSecret } from '../utils/jwt-secret'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor( + private readonly reflector: Reflector, + private readonly jwtService: JwtService, + private readonly config: ConfigService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + const request = context.switchToHttp().getRequest<{ + headers: Record; + user?: AuthPrincipal; + }>(); + const authHeader = request.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException('توکن احراز هویت الزامی است'); + } + + const token = authHeader.slice(7); + let payload: AuthPrincipal & { sub?: string }; + try { + payload = await this.jwtService.verifyAsync(token, { + secret: resolveJwtSecret(this.config), + }); + } catch { + throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است'); + } + + const user: AuthPrincipal = { + id: payload.sub ?? payload.id, + type: payload.type, + role: payload.role, + phone: payload.phone, + }; + request.user = user; + + const authType = this.reflector.getAllAndOverride< + 'customer' | 'admin' | 'any' + >(AUTH_TYPE_KEY, [context.getHandler(), context.getClass()]); + if (authType && authType !== 'any' && user.type !== authType) { + throw new ForbiddenException('دسترسی مجاز نیست'); + } + + const roles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (roles?.length && (!user.role || !roles.includes(user.role))) { + throw new ForbiddenException('نقش کاربری کافی نیست'); + } + + return true; + } +} diff --git a/src/common/utils/cash-expiry.ts b/src/common/utils/cash-expiry.ts new file mode 100644 index 0000000..9212d09 --- /dev/null +++ b/src/common/utils/cash-expiry.ts @@ -0,0 +1,39 @@ +import { addDays, tehranAtHour } from './tehran-time'; + +export function computeCashInvoiceExpiry(company: { + cashInvoiceValidUntilHour: number | null; + cashInvoiceValidityHours: number; +}): Date { + const now = new Date(); + if ( + company.cashInvoiceValidUntilHour != null && + Number.isFinite(company.cashInvoiceValidUntilHour) + ) { + const expires = tehranAtHour(now, company.cashInvoiceValidUntilHour); + if (expires.getTime() <= now.getTime()) { + return tehranAtHour(addDays(now, 1), company.cashInvoiceValidUntilHour); + } + return expires; + } + + return new Date( + now.getTime() + company.cashInvoiceValidityHours * 60 * 60 * 1000, + ); +} + +/** برای فاکتور چندشرکتی نزدیک‌ترین مهلت را انتخاب می‌کند. */ +export function earliestCashInvoiceExpiry( + companies: Array<{ + cashInvoiceValidUntilHour: number | null; + cashInvoiceValidityHours: number; + }>, +): Date { + if (!companies.length) { + return new Date(Date.now() + 8 * 60 * 60 * 1000); + } + return companies + .map((company) => computeCashInvoiceExpiry(company)) + .reduce((earliest, current) => + current.getTime() < earliest.getTime() ? current : earliest, + ); +} diff --git a/src/common/utils/check-rass.ts b/src/common/utils/check-rass.ts new file mode 100644 index 0000000..cf8a4e0 --- /dev/null +++ b/src/common/utils/check-rass.ts @@ -0,0 +1,62 @@ +export type CheckRassInput = { + amount: number; + dueDate: Date | string; +}; + +export type CheckRassResult = { + checkCount: number; + totalAmount: number; + nearestDueDate: string | null; + farthestDueDate: string | null; + rassDays: number; + rassDate: string | null; +}; + +import { + addDays, + calendarDayOf, + diffInDays, + tehranCalendarDay, +} from './tehran-time'; + +export function calculateCheckRass( + checks: CheckRassInput[], + baseDate: Date = new Date(), +): CheckRassResult { + if (checks.length === 0) { + return { + checkCount: 0, + totalAmount: 0, + nearestDueDate: null, + farthestDueDate: null, + rassDays: 0, + rassDate: null, + }; + } + + const base = tehranCalendarDay(baseDate); + let totalAmount = 0; + let weightedDays = 0; + const dueDates: Date[] = []; + + for (const check of checks) { + const due = calendarDayOf(check.dueDate); + dueDates.push(due); + const days = Math.max(0, diffInDays(base, due)); + totalAmount += check.amount; + weightedDays += check.amount * days; + } + + const rassDays = totalAmount > 0 ? Math.round(weightedDays / totalAmount) : 0; + const rassDate = addDays(base, rassDays); + dueDates.sort((a, b) => a.getTime() - b.getTime()); + + return { + checkCount: checks.length, + totalAmount, + nearestDueDate: dueDates[0]?.toISOString() ?? null, + farthestDueDate: dueDates[dueDates.length - 1]?.toISOString() ?? null, + rassDays, + rassDate: rassDate.toISOString(), + }; +} diff --git a/src/common/utils/jwt-secret.ts b/src/common/utils/jwt-secret.ts new file mode 100644 index 0000000..f865c6d --- /dev/null +++ b/src/common/utils/jwt-secret.ts @@ -0,0 +1,16 @@ +import { ConfigService } from '@nestjs/config'; + +const DEV_FALLBACK = 'fabric-trade-dev-secret'; + +/** + * امضا و اعتبارسنجی توکن باید از یک منبع بخوانند؛ در production نبود مقدار خطای راه‌اندازی است. + */ +export function resolveJwtSecret(config: ConfigService): string { + const secret = config.get('JWT_SECRET')?.trim(); + if (secret) return secret; + + if (config.get('NODE_ENV') === 'production') { + throw new Error('JWT_SECRET تنظیم نشده است'); + } + return DEV_FALLBACK; +} diff --git a/src/common/utils/meterage.ts b/src/common/utils/meterage.ts new file mode 100644 index 0000000..3fd0247 --- /dev/null +++ b/src/common/utils/meterage.ts @@ -0,0 +1,14 @@ +/** وقتی نه شرکت و نه تنظیمات سیستم مقداری ندارند. */ +export const FALLBACK_MAX_METERAGE = 5000; + +/** + * سقف متراژ موثر هر فاکتور: مقدار شرکت، در نبودش تنظیمات سیستم. + */ +export function resolveMaxMeterage( + companyValue: number | null | undefined, + settingsValue: number | null | undefined, +): number { + if (companyValue != null && companyValue > 0) return companyValue; + if (settingsValue != null && settingsValue > 0) return settingsValue; + return FALLBACK_MAX_METERAGE; +} diff --git a/src/common/utils/name.ts b/src/common/utils/name.ts new file mode 100644 index 0000000..9851842 --- /dev/null +++ b/src/common/utils/name.ts @@ -0,0 +1,21 @@ +export function splitFullName(name: string): { + firstName: string; + lastName: string; +} { + const trimmed = name.trim().replace(/\s+/g, ' '); + if (!trimmed) { + return { firstName: 'مشتری', lastName: 'جدید' }; + } + const parts = trimmed.split(' '); + if (parts.length === 1) { + return { firstName: parts[0], lastName: '' }; + } + return { + firstName: parts[0], + lastName: parts.slice(1).join(' '), + }; +} + +export function joinFullName(firstName: string, lastName: string): string { + return `${firstName} ${lastName}`.trim(); +} diff --git a/src/common/utils/order-number.ts b/src/common/utils/order-number.ts new file mode 100644 index 0000000..f247bdf --- /dev/null +++ b/src/common/utils/order-number.ts @@ -0,0 +1,8 @@ +export function generateOrderNumber(date: Date = new Date()): string { + const year = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { + year: 'numeric', + }).format(date); + const persianYear = year.replace(/[^\d]/g, '') || '1403'; + const seq = Math.floor(Math.random() * 9000) + 1000; + return `ORD-${persianYear}-${seq}`; +} diff --git a/src/common/utils/order-status.ts b/src/common/utils/order-status.ts new file mode 100644 index 0000000..877aa76 --- /dev/null +++ b/src/common/utils/order-status.ts @@ -0,0 +1,90 @@ +import { OrderStatus } from '@prisma/client'; + +/** Admin UI simplified statuses → full Prisma OrderStatus values */ +export const ADMIN_STATUS_MAP: Record = { + pending_review: [ + OrderStatus.cart, + OrderStatus.submitted, + OrderStatus.awaiting_review, + OrderStatus.pricing, + ], + awaiting_customer: [ + OrderStatus.awaiting_settlement, + OrderStatus.awaiting_settlement_fix, + OrderStatus.check_in_progress, + OrderStatus.cash_in_progress, + OrderStatus.cash_invoice_issued, + OrderStatus.awaiting_cash_payment, + ], + pending_check: [OrderStatus.awaiting_check_review], + pending_payment: [ + OrderStatus.cash_payment_submitted, + OrderStatus.awaiting_cash_confirmation, + ], + needs_correction: [OrderStatus.needs_correction], + check_correction: [OrderStatus.check_needs_fix, OrderStatus.check_rejected], + final_approval: [ + OrderStatus.final_approved, + OrderStatus.checks_approved, + OrderStatus.cash_confirmed, + OrderStatus.preparing, + OrderStatus.ready_to_ship, + ], + shipped: [OrderStatus.shipped], + delivered: [OrderStatus.delivered], +}; + +/** وضعیت‌های پایانی عمداً در تب‌های جریان کاری نمی‌آیند و با برچسب خودشان نمایش داده می‌شوند. */ +const TERMINAL_STATUSES: OrderStatus[] = [ + OrderStatus.cancelled, + OrderStatus.rejected, + OrderStatus.expired, +]; + +export function resolveAdminStatusFilter( + status?: string, +): OrderStatus[] | undefined { + if (!status) return undefined; + if (ADMIN_STATUS_MAP[status]) return ADMIN_STATUS_MAP[status]; + if (Object.values(OrderStatus).includes(status as OrderStatus)) { + return [status as OrderStatus]; + } + return undefined; +} + +export function toAdminStatus(status: OrderStatus): string { + for (const [admin, list] of Object.entries(ADMIN_STATUS_MAP)) { + if (list.includes(status)) return admin; + } + if (TERMINAL_STATUSES.includes(status)) return status; + return 'pending_review'; +} + +export const ORDER_STATUS_LABELS: Record = { + 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: 'منقضی', +}; diff --git a/src/common/utils/order-transitions.ts b/src/common/utils/order-transitions.ts new file mode 100644 index 0000000..a782999 --- /dev/null +++ b/src/common/utils/order-transitions.ts @@ -0,0 +1,101 @@ +import { BadRequestException } from '@nestjs/common'; +import { OrderStatus } from '@prisma/client'; +import { ORDER_STATUS_LABELS } from './order-status'; + +/** + * گذرهای مجاز گردش سفارش. enum دیتابیس وضعیت‌های بیشتری دارد که از نسخه‌های + * قبلی مانده‌اند؛ فقط همین‌ها در جریان واقعی استفاده می‌شوند. + * + * ترتیب کار: ثبت سفارش ← قیمت‌گذاری ادمین ← ارسال فاکتور ← انتخاب تسویه توسط + * مشتری ← شاخه نقدی یا چکی ← تأیید نهایی ← ارسال. + */ +export const ORDER_TRANSITIONS: Record = { + awaiting_review: [OrderStatus.pricing], + pricing: [OrderStatus.awaiting_settlement], + awaiting_settlement: [ + OrderStatus.awaiting_cash_payment, + OrderStatus.check_in_progress, + OrderStatus.pricing, + ], + + // شاخه نقدی + awaiting_cash_payment: [ + OrderStatus.awaiting_cash_confirmation, + OrderStatus.expired, + ], + awaiting_cash_confirmation: [ + OrderStatus.cash_confirmed, + OrderStatus.awaiting_cash_payment, + ], + cash_confirmed: [OrderStatus.final_approved], + + // شاخه چکی + check_in_progress: [OrderStatus.awaiting_check_review], + awaiting_check_review: [ + OrderStatus.checks_approved, + OrderStatus.check_needs_fix, + ], + check_needs_fix: [OrderStatus.awaiting_check_review], + checks_approved: [OrderStatus.final_approved], + + // مسیر مشترک تا تحویل + final_approved: [OrderStatus.preparing], + preparing: [OrderStatus.ready_to_ship], + ready_to_ship: [OrderStatus.shipped], + shipped: [OrderStatus.delivered], + delivered: [], + + // پایانی‌ها + cancelled: [], + rejected: [], + expired: [], + + // وضعیت‌های قدیمی: فقط راه خروج دارند تا رکوردهای موجود قفل نشوند + cart: [OrderStatus.awaiting_review], + submitted: [OrderStatus.awaiting_review], + needs_correction: [OrderStatus.awaiting_review, OrderStatus.pricing], + awaiting_settlement_fix: [OrderStatus.awaiting_settlement], + cash_in_progress: [OrderStatus.awaiting_cash_payment], + cash_invoice_issued: [OrderStatus.awaiting_cash_payment], + cash_payment_submitted: [OrderStatus.awaiting_cash_confirmation], + check_rejected: [OrderStatus.check_needs_fix], +}; + +/** ادمین در هر مرحله‌ای می‌تواند سفارش را ببندد. */ +const ADMIN_TERMINAL_TRANSITIONS: OrderStatus[] = [ + OrderStatus.cancelled, + OrderStatus.rejected, +]; + +const FINAL_STATUSES: OrderStatus[] = [ + OrderStatus.delivered, + OrderStatus.cancelled, + OrderStatus.rejected, + OrderStatus.expired, +]; + +export function isFinalStatus(status: OrderStatus): boolean { + return FINAL_STATUSES.includes(status); +} + +/** گذرهایی که ادمین بدون حالت اجباری می‌تواند انتخاب کند. */ +export function allowedTransitions(from: OrderStatus): OrderStatus[] { + const next = [...(ORDER_TRANSITIONS[from] ?? [])]; + if (!isFinalStatus(from)) { + for (const terminal of ADMIN_TERMINAL_TRANSITIONS) { + if (!next.includes(terminal)) next.push(terminal); + } + } + return next; +} + +export function canTransition(from: OrderStatus, to: OrderStatus): boolean { + return allowedTransitions(from).includes(to); +} + +export function assertTransition(from: OrderStatus, to: OrderStatus): void { + if (canTransition(from, to)) return; + throw new BadRequestException( + `تغییر وضعیت از «${ORDER_STATUS_LABELS[from]}» به «${ORDER_STATUS_LABELS[to]}» مجاز نیست`, + ); +} diff --git a/src/common/utils/phone.ts b/src/common/utils/phone.ts new file mode 100644 index 0000000..2586ad0 --- /dev/null +++ b/src/common/utils/phone.ts @@ -0,0 +1,20 @@ +export function normalizePhone(raw: string): string { + let digits = raw.replace(/\D/g, ''); + if (digits.startsWith('98') && digits.length === 12) { + digits = `0${digits.slice(2)}`; + } + if (digits.length === 10 && digits.startsWith('9')) { + digits = `0${digits}`; + } + return digits; +} + +export function isValidPhone(phone: string): boolean { + return /^09\d{9}$/.test(phone); +} + +export function toEnglishDigits(value: string): string { + return value + .replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))) + .replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))); +} diff --git a/src/common/utils/tehran-time.ts b/src/common/utils/tehran-time.ts new file mode 100644 index 0000000..663ab19 --- /dev/null +++ b/src/common/utils/tehran-time.ts @@ -0,0 +1,42 @@ +/** + * محاسبات تقویمی باید مستقل از تایم‌زون سرور باشد؛ کانتینرها روی UTC اجرا می‌شوند. + * ایران از ۱۴۰۱ تغییر ساعت تابستانی ندارد، پس آفست ثابت +۰۳:۳۰ درست است. + */ +const TEHRAN_OFFSET_MS = (3 * 60 + 30) * 60 * 1000; +const DAY_MS = 24 * 60 * 60 * 1000; + +function utcStartOfDay(date: Date): Date { + const d = new Date(date); + d.setUTCHours(0, 0, 0, 0); + return d; +} + +/** + * روز تقویمی تهرانِ یک لحظه را به‌صورت نیمه‌شب UTC همان روز برمی‌گرداند. + * خروجی برای اختلاف‌گیری روزانه و سریال‌سازی `YYYY-MM-DD` امن است. + */ +export function tehranCalendarDay(date: Date): Date { + return utcStartOfDay(new Date(date.getTime() + TEHRAN_OFFSET_MS)); +} + +/** تاریخ سررسید فقط-روز است و نیمه‌شب UTC ذخیره می‌شود. */ +export function calendarDayOf(date: Date | string): Date { + return utcStartOfDay(new Date(date)); +} + +export function addDays(date: Date, days: number): Date { + return new Date(date.getTime() + days * DAY_MS); +} + +export function diffInDays(from: Date, to: Date): number { + return Math.round((to.getTime() - from.getTime()) / DAY_MS); +} + +/** لحظه‌ای که در تهران ساعتِ داده‌شده از همان روزِ تقویمی است. */ +export function tehranAtHour(date: Date, hour: number): Date { + return new Date( + tehranCalendarDay(date).getTime() + + hour * 60 * 60 * 1000 - + TEHRAN_OFFSET_MS, + ); +} diff --git a/src/companies/companies.controller.ts b/src/companies/companies.controller.ts new file mode 100644 index 0000000..a6ecc2e --- /dev/null +++ b/src/companies/companies.controller.ts @@ -0,0 +1,93 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { CompaniesService } from './companies.service'; +import { AuthType, Public } from '../common/decorators/auth.decorators'; +import { + CreateBankAccountDto, + CreateCompanyDto, + SetActiveDto, + UpdateBankAccountDto, + UpdateCompanyDto, +} from './dto/company.dto'; + +@Controller('companies') +export class CompaniesController { + constructor(private readonly companiesService: CompaniesService) {} + + @AuthType('any') + @Get() + findAll(@Query('activeOnly') activeOnly?: string) { + return this.companiesService.findAll( + activeOnly === 'true' || activeOnly === '1', + ); + } + + @Public() + @Get('public') + findPublic() { + return this.companiesService.findAll(true); + } + + @AuthType('any') + @Get(':id') + findOne(@Param('id') id: string) { + return this.companiesService.findOne(id); + } + + @AuthType('admin') + @Post() + create(@Body() dto: CreateCompanyDto) { + return this.companiesService.create(dto); + } + + @AuthType('admin') + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateCompanyDto) { + return this.companiesService.update(id, dto); + } + + @AuthType('admin') + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetActiveDto) { + return this.companiesService.setActive(id, dto.isActive); + } + + @AuthType('any') + @Get(':companyId/bank-accounts') + listBankAccounts(@Param('companyId') companyId: string) { + return this.companiesService.listBankAccounts(companyId); + } + + @AuthType('admin') + @Post(':companyId/bank-accounts') + createBankAccount( + @Param('companyId') companyId: string, + @Body() dto: CreateBankAccountDto, + ) { + return this.companiesService.createBankAccount(companyId, dto); + } +} + +@Controller('bank-accounts') +export class BankAccountsController { + constructor(private readonly companiesService: CompaniesService) {} + + @AuthType('admin') + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateBankAccountDto) { + return this.companiesService.updateBankAccount(id, dto); + } + + @AuthType('admin') + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetActiveDto) { + return this.companiesService.setBankAccountActive(id, dto.isActive); + } +} diff --git a/src/companies/companies.module.ts b/src/companies/companies.module.ts new file mode 100644 index 0000000..24b100f --- /dev/null +++ b/src/companies/companies.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { + BankAccountsController, + CompaniesController, +} from './companies.controller'; +import { CompaniesService } from './companies.service'; + +@Module({ + controllers: [CompaniesController, BankAccountsController], + providers: [CompaniesService], + exports: [CompaniesService], +}) +export class CompaniesModule {} diff --git a/src/companies/companies.service.ts b/src/companies/companies.service.ts new file mode 100644 index 0000000..3691681 --- /dev/null +++ b/src/companies/companies.service.ts @@ -0,0 +1,287 @@ +import { + Injectable, + NotFoundException, + ConflictException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { resolveMaxMeterage } from '../common/utils/meterage'; +import { + CreateBankAccountDto, + CreateCompanyDto, + UpdateBankAccountDto, + UpdateCompanyDto, +} from './dto/company.dto'; + +@Injectable() +export class CompaniesService { + constructor(private readonly prisma: PrismaService) {} + + async findAll(activeOnly = false) { + const [companies, fallbackMeterage] = await Promise.all([ + this.prisma.company.findMany({ + where: activeOnly ? { isActive: true } : undefined, + orderBy: [{ displayOrder: 'asc' }, { name: 'asc' }], + include: { + _count: { + select: { catalogs: true, products: true, bankAccounts: true }, + }, + }, + }), + this.settingsMaxMeterage(), + ]); + return companies.map((c) => this.mapCompany(c, fallbackMeterage)); + } + + async findOne(id: string) { + const [company, fallbackMeterage] = await Promise.all([ + this.prisma.company.findUnique({ + where: { id }, + include: { + bankAccounts: { orderBy: { displayOrder: 'asc' } }, + _count: { + select: { catalogs: true, products: true, bankAccounts: true }, + }, + }, + }), + this.settingsMaxMeterage(), + ]); + if (!company) throw new NotFoundException('شرکت یافت نشد'); + return { + ...this.mapCompany(company, fallbackMeterage), + bankAccounts: company.bankAccounts.map((b) => this.mapBankAccount(b)), + }; + } + + /** سقف متراژ تنظیمات سیستم که شرکت‌های بدون سقف اختصاصی از آن ارث می‌برند. */ + private async settingsMaxMeterage(): Promise { + const settings = await this.prisma.systemSettings.findUnique({ + where: { id: 'default' }, + select: { maxMeteragePerInvoice: true }, + }); + return settings?.maxMeteragePerInvoice ?? null; + } + + async create(dto: CreateCompanyDto) { + try { + const company = await this.prisma.company.create({ + data: { + name: dto.name.trim(), + slug: dto.slug.trim(), + description: dto.description?.trim() || null, + logoUrl: dto.logoUrl?.trim() || null, + displayOrder: dto.displayOrder ?? 0, + maxMeteragePerInvoice: dto.maxMeteragePerInvoice ?? null, + cashInvoiceValidityHours: dto.cashInvoiceValidityHours ?? 8, + cashInvoiceValidUntilHour: dto.cashInvoiceValidUntilHour ?? null, + isActive: dto.isActive ?? true, + }, + include: { + _count: { + select: { catalogs: true, products: true, bankAccounts: true }, + }, + }, + }); + return this.mapCompany(company); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('اسلاگ شرکت تکراری است'); + } + throw e; + } + } + + async update(id: string, dto: UpdateCompanyDto) { + await this.ensureCompany(id); + try { + const company = await this.prisma.company.update({ + where: { id }, + data: { + name: dto.name?.trim(), + slug: dto.slug?.trim(), + description: + dto.description === undefined + ? undefined + : dto.description?.trim() || null, + logoUrl: + dto.logoUrl === undefined ? undefined : dto.logoUrl?.trim() || null, + displayOrder: dto.displayOrder, + maxMeteragePerInvoice: dto.maxMeteragePerInvoice, + cashInvoiceValidityHours: dto.cashInvoiceValidityHours, + cashInvoiceValidUntilHour: dto.cashInvoiceValidUntilHour, + isActive: dto.isActive, + }, + include: { + _count: { + select: { catalogs: true, products: true, bankAccounts: true }, + }, + }, + }); + return this.mapCompany(company); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('اسلاگ شرکت تکراری است'); + } + throw e; + } + } + + async setActive(id: string, isActive: boolean) { + await this.ensureCompany(id); + const company = await this.prisma.company.update({ + where: { id }, + data: { isActive }, + include: { + _count: { + select: { catalogs: true, products: true, bankAccounts: true }, + }, + }, + }); + return this.mapCompany(company); + } + + async listBankAccounts(companyId: string) { + await this.ensureCompany(companyId); + const accounts = await this.prisma.bankAccount.findMany({ + where: { companyId }, + orderBy: { displayOrder: 'asc' }, + }); + return accounts.map((a) => this.mapBankAccount(a)); + } + + async createBankAccount(companyId: string, dto: CreateBankAccountDto) { + await this.ensureCompany(companyId); + const account = await this.prisma.bankAccount.create({ + data: { + companyId, + bankName: dto.bankName.trim(), + accountHolder: dto.accountHolder.trim(), + accountNumber: dto.accountNumber.trim(), + cardNumber: dto.cardNumber.trim(), + iban: dto.iban.trim(), + notes: dto.notes?.trim() || null, + displayOrder: dto.displayOrder ?? 0, + isActive: dto.isActive ?? true, + }, + }); + return this.mapBankAccount(account); + } + + async updateBankAccount(id: string, dto: UpdateBankAccountDto) { + await this.ensureBankAccount(id); + const account = await this.prisma.bankAccount.update({ + where: { id }, + data: { + bankName: dto.bankName?.trim(), + accountHolder: dto.accountHolder?.trim(), + accountNumber: dto.accountNumber?.trim(), + cardNumber: dto.cardNumber?.trim(), + iban: dto.iban?.trim(), + notes: dto.notes === undefined ? undefined : dto.notes?.trim() || null, + displayOrder: dto.displayOrder, + isActive: dto.isActive, + }, + }); + return this.mapBankAccount(account); + } + + async setBankAccountActive(id: string, isActive: boolean) { + await this.ensureBankAccount(id); + const account = await this.prisma.bankAccount.update({ + where: { id }, + data: { isActive }, + }); + return this.mapBankAccount(account); + } + + private async ensureCompany(id: string) { + const company = await this.prisma.company.findUnique({ where: { id } }); + if (!company) throw new NotFoundException('شرکت یافت نشد'); + return company; + } + + private async ensureBankAccount(id: string) { + const account = await this.prisma.bankAccount.findUnique({ where: { id } }); + if (!account) throw new NotFoundException('حساب بانکی یافت نشد'); + return account; + } + + private mapCompany( + company: { + id: string; + name: string; + slug: string; + description: string | null; + logoUrl: string | null; + isActive: boolean; + displayOrder: number; + maxMeteragePerInvoice: number | null; + cashInvoiceValidityHours: number; + cashInvoiceValidUntilHour: number | null; + createdAt: Date; + updatedAt: Date; + _count?: { catalogs: number; products: number; bankAccounts: number }; + }, + fallbackMaxMeterage: number | null = null, + ) { + return { + id: company.id, + name: company.name, + slug: company.slug, + description: company.description ?? '', + logoUrl: company.logoUrl, + logo: company.logoUrl ?? undefined, + isActive: company.isActive, + displayOrder: company.displayOrder, + maxMeteragePerInvoice: resolveMaxMeterage( + company.maxMeteragePerInvoice, + fallbackMaxMeterage, + ), + /** مقدار خام شرکت؛ خالی یعنی از تنظیمات سیستم ارث می‌برد */ + ownMaxMeteragePerInvoice: company.maxMeteragePerInvoice ?? undefined, + cashInvoiceValidityHours: company.cashInvoiceValidityHours, + cashInvoiceValidUntilHour: company.cashInvoiceValidUntilHour ?? undefined, + catalogCount: company._count?.catalogs ?? 0, + productCount: company._count?.products ?? 0, + bankAccountCount: company._count?.bankAccounts ?? 0, + createdAt: company.createdAt.toISOString(), + updatedAt: company.updatedAt.toISOString(), + }; + } + + private mapBankAccount(account: { + id: string; + companyId: string; + bankName: string; + accountHolder: string; + accountNumber: string; + cardNumber: string; + iban: string; + notes: string | null; + isActive: boolean; + displayOrder: number; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: account.id, + companyId: account.companyId, + bankName: account.bankName, + accountHolder: account.accountHolder, + accountNumber: account.accountNumber, + cardNumber: account.cardNumber, + iban: account.iban, + notes: account.notes ?? undefined, + isActive: account.isActive, + displayOrder: account.displayOrder, + createdAt: account.createdAt.toISOString(), + updatedAt: account.updatedAt.toISOString(), + }; + } +} diff --git a/src/companies/dto/company.dto.ts b/src/companies/dto/company.dto.ts new file mode 100644 index 0000000..41d5b2f --- /dev/null +++ b/src/companies/dto/company.dto.ts @@ -0,0 +1,176 @@ +import { + IsBoolean, + IsInt, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateCompanyDto { + @IsString() + @MinLength(1) + name!: string; + + @IsString() + @MinLength(1) + slug!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + logoUrl?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + maxMeteragePerInvoice?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + cashInvoiceValidityHours?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + cashInvoiceValidUntilHour?: number | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateCompanyDto { + @IsOptional() + @IsString() + @MinLength(1) + name?: string; + + @IsOptional() + @IsString() + @MinLength(1) + slug?: string; + + @IsOptional() + @IsString() + description?: string | null; + + @IsOptional() + @IsString() + logoUrl?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + maxMeteragePerInvoice?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + cashInvoiceValidityHours?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + cashInvoiceValidUntilHour?: number | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class SetActiveDto { + @IsBoolean() + isActive!: boolean; +} + +export class CreateBankAccountDto { + @IsString() + @MinLength(1) + bankName!: string; + + @IsString() + @MinLength(1) + accountHolder!: string; + + @IsString() + @MinLength(1) + accountNumber!: string; + + @IsString() + @MinLength(1) + cardNumber!: string; + + @IsString() + @MinLength(1) + iban!: string; + + @IsOptional() + @IsString() + notes?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} + +export class UpdateBankAccountDto { + @IsOptional() + @IsString() + bankName?: string; + + @IsOptional() + @IsString() + accountHolder?: string; + + @IsOptional() + @IsString() + accountNumber?: string; + + @IsOptional() + @IsString() + cardNumber?: string; + + @IsOptional() + @IsString() + iban?: string; + + @IsOptional() + @IsString() + notes?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + displayOrder?: number; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/src/customers/customers.controller.ts b/src/customers/customers.controller.ts new file mode 100644 index 0000000..be1b68b --- /dev/null +++ b/src/customers/customers.controller.ts @@ -0,0 +1,52 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { AppUserRole } from '@prisma/client'; +import { CustomersService } from './customers.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { + CreateCustomerDto, + SetCustomerActiveDto, + UpdateCustomerDto, +} from './dto/customer.dto'; + +@Controller('admin/customers') +@AuthType('admin') +@Roles('admin', 'sales') +export class CustomersController { + constructor(private readonly customersService: CustomersService) {} + + @Get() + findAll( + @Query('q') q?: string, + @Query('appRole') appRole?: AppUserRole, + ) { + return this.customersService.findAll(q, appRole); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.customersService.findOne(id); + } + + @Post() + create(@Body() dto: CreateCustomerDto) { + return this.customersService.create(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateCustomerDto) { + return this.customersService.update(id, dto); + } + + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetCustomerActiveDto) { + return this.customersService.setActive(id, dto.isActive); + } +} diff --git a/src/customers/customers.module.ts b/src/customers/customers.module.ts new file mode 100644 index 0000000..bb9a343 --- /dev/null +++ b/src/customers/customers.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CustomersController } from './customers.controller'; +import { CustomersService } from './customers.service'; + +@Module({ + controllers: [CustomersController], + providers: [CustomersService], + exports: [CustomersService], +}) +export class CustomersModule {} diff --git a/src/customers/customers.service.ts b/src/customers/customers.service.ts new file mode 100644 index 0000000..c710923 --- /dev/null +++ b/src/customers/customers.service.ts @@ -0,0 +1,314 @@ +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { AppUserRole, CustomerType, Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateCustomerDto, UpdateCustomerDto } from './dto/customer.dto'; +import { + isValidPhone, + normalizePhone, + toEnglishDigits, +} from '../common/utils/phone'; +import { joinFullName, splitFullName } from '../common/utils/name'; + +const customerInclude = { + company: { select: { name: true } }, + orders: { select: { totalMeterage: true } }, + levelDef: { select: { id: true, code: true, label: true } }, + assignedAgent: { + select: { id: true, firstName: true, lastName: true, phone: true, agentCode: true }, + }, + customerBrands: { + include: { brand: { select: { id: true, name: true } } }, + }, +} satisfies Prisma.CustomerInclude; + +type CustomerWithRelations = Prisma.CustomerGetPayload<{ + include: typeof customerInclude; +}>; + +@Injectable() +export class CustomersService { + constructor(private readonly prisma: PrismaService) {} + + async findAll(q?: string, appRole?: AppUserRole) { + const customers = await this.prisma.customer.findMany({ + where: { + ...(appRole ? { appRole } : {}), + ...(q + ? { + OR: [ + { firstName: { contains: q, mode: 'insensitive' } }, + { lastName: { contains: q, mode: 'insensitive' } }, + { phone: { contains: toEnglishDigits(q) } }, + { nationalId: { contains: toEnglishDigits(q) } }, + { agentCode: { contains: q, mode: 'insensitive' } }, + ], + } + : {}), + }, + orderBy: { createdAt: 'desc' }, + include: customerInclude, + }); + return customers.map((c) => this.map(c)); + } + + async findOne(id: string) { + const customer = await this.prisma.customer.findUnique({ + where: { id }, + include: customerInclude, + }); + if (!customer) throw new NotFoundException('مشتری یافت نشد'); + return this.map(customer); + } + + async create(dto: CreateCustomerDto) { + const phone = normalizePhone(toEnglishDigits(dto.phone)); + if (!isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + + const { firstName, lastName } = this.resolveNames(dto); + await this.assertCompany(dto.companyId); + await this.assertLevel(dto.levelDefId); + await this.assertAgent(dto.assignedAgentId); + + try { + const customer = await this.prisma.$transaction(async (tx) => { + const created = await tx.customer.create({ + data: { + firstName, + lastName, + phone, + nationalId: dto.nationalId?.trim() || null, + customerType: dto.customerType ?? CustomerType.wholesale, + companyId: dto.companyId || null, + isActive: dto.isActive ?? true, + levelDefId: dto.levelDefId || null, + assignedAgentId: dto.assignedAgentId || null, + appRole: dto.appRole ?? AppUserRole.customer, + agentCode: dto.agentCode?.trim() || null, + }, + }); + if (dto.brandIds?.length) { + await tx.customerBrand.createMany({ + data: dto.brandIds.map((brandId) => ({ + customerId: created.id, + brandId, + })), + skipDuplicates: true, + }); + } + return tx.customer.findUniqueOrThrow({ + where: { id: created.id }, + include: customerInclude, + }); + }); + return this.map(customer); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('این شماره قبلاً ثبت شده است'); + } + throw e; + } + } + + async update(id: string, dto: UpdateCustomerDto) { + await this.ensure(id); + const phone = + dto.phone !== undefined + ? normalizePhone(toEnglishDigits(dto.phone)) + : undefined; + if (phone !== undefined && !isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + + let firstName: string | undefined; + let lastName: string | undefined; + if ( + dto.name !== undefined || + dto.firstName !== undefined || + dto.lastName !== undefined + ) { + const names = this.resolveNames(dto); + firstName = names.firstName; + lastName = names.lastName; + } + + if (dto.companyId !== undefined) await this.assertCompany(dto.companyId); + if (dto.levelDefId !== undefined) await this.assertLevel(dto.levelDefId); + if (dto.assignedAgentId !== undefined) { + await this.assertAgent(dto.assignedAgentId, id); + } + + try { + const customer = await this.prisma.$transaction(async (tx) => { + await tx.customer.update({ + where: { id }, + data: { + firstName, + lastName, + phone, + nationalId: + dto.nationalId === undefined + ? undefined + : dto.nationalId?.trim() || null, + customerType: dto.customerType, + companyId: + dto.companyId === undefined ? undefined : dto.companyId || null, + isActive: dto.isActive, + levelDefId: + dto.levelDefId === undefined ? undefined : dto.levelDefId || null, + assignedAgentId: + dto.assignedAgentId === undefined + ? undefined + : dto.assignedAgentId || null, + appRole: dto.appRole, + agentCode: + dto.agentCode === undefined + ? undefined + : dto.agentCode?.trim() || null, + }, + }); + + if (dto.brandIds !== undefined) { + await tx.customerBrand.deleteMany({ where: { customerId: id } }); + if (dto.brandIds.length) { + await tx.customerBrand.createMany({ + data: dto.brandIds.map((brandId) => ({ + customerId: id, + brandId, + })), + skipDuplicates: true, + }); + } + } + + return tx.customer.findUniqueOrThrow({ + where: { id }, + include: customerInclude, + }); + }); + return this.map(customer); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('این شماره قبلاً ثبت شده است'); + } + throw e; + } + } + + async setActive(id: string, isActive: boolean) { + await this.ensure(id); + const customer = await this.prisma.customer.update({ + where: { id }, + data: { isActive }, + include: customerInclude, + }); + return this.map(customer); + } + + private resolveNames(dto: { + name?: string; + firstName?: string; + lastName?: string; + }) { + if (dto.name?.trim()) { + return splitFullName(dto.name); + } + const firstName = dto.firstName?.trim(); + const lastName = dto.lastName?.trim() ?? ''; + if (!firstName) { + throw new BadRequestException('نام مشتری الزامی است'); + } + return { firstName, lastName }; + } + + private async ensure(id: string) { + const customer = await this.prisma.customer.findUnique({ where: { id } }); + if (!customer) throw new NotFoundException('مشتری یافت نشد'); + return customer; + } + + private async assertCompany(companyId?: string | null) { + if (!companyId) return; + const company = await this.prisma.company.findUnique({ + where: { id: companyId }, + }); + if (!company) throw new NotFoundException('شرکت یافت نشد'); + } + + private async assertLevel(levelDefId?: string | null) { + if (!levelDefId) return; + const level = await this.prisma.customerLevelDef.findUnique({ + where: { id: levelDefId }, + }); + if (!level) throw new NotFoundException('سطح مشتری یافت نشد'); + } + + private async assertAgent(agentId?: string | null, customerId?: string) { + if (!agentId) return; + if (customerId && agentId === customerId) { + throw new BadRequestException('واسطه نمی‌تواند خودش باشد'); + } + const agent = await this.prisma.customer.findUnique({ + where: { id: agentId }, + }); + if (!agent || agent.appRole !== AppUserRole.agent) { + throw new BadRequestException('واسطه معتبر نیست'); + } + } + + private map(customer: CustomerWithRelations) { + const totalMeterage = customer.orders.reduce( + (sum, o) => sum + o.totalMeterage, + 0, + ); + const name = joinFullName(customer.firstName, customer.lastName); + const agentName = customer.assignedAgent + ? joinFullName( + customer.assignedAgent.firstName, + customer.assignedAgent.lastName, + ) + : ''; + return { + id: customer.id, + firstName: customer.firstName, + lastName: customer.lastName, + name, + phone: customer.phone, + nationalId: customer.nationalId ?? '', + customerType: customer.customerType, + companyId: customer.companyId, + companyName: customer.company?.name ?? '', + orderCount: customer.orders.length, + totalMeterage, + isActive: customer.isActive, + appRole: customer.appRole, + agentCode: customer.agentCode ?? '', + levelDefId: customer.levelDefId, + levelCode: customer.levelDef?.code ?? '', + levelLabel: customer.levelDef?.label ?? '', + assignedAgentId: customer.assignedAgentId, + assignedAgentName: agentName, + assignedAgentPhone: customer.assignedAgent?.phone ?? '', + brandIds: customer.customerBrands.map((cb) => cb.brandId), + brands: customer.customerBrands.map((cb) => ({ + id: cb.brand.id, + name: cb.brand.name, + })), + registeredAt: customer.createdAt.toISOString(), + createdAt: customer.createdAt.toISOString(), + updatedAt: customer.updatedAt.toISOString(), + }; + } +} diff --git a/src/customers/dto/customer.dto.ts b/src/customers/dto/customer.dto.ts new file mode 100644 index 0000000..db346ff --- /dev/null +++ b/src/customers/dto/customer.dto.ts @@ -0,0 +1,125 @@ +import { + IsArray, + IsBoolean, + IsEnum, + IsOptional, + IsString, + MinLength, +} from 'class-validator'; +import { AppUserRole, CustomerType } from '@prisma/client'; + +export class CreateCustomerDto { + @IsOptional() + @IsString() + firstName?: string; + + @IsOptional() + @IsString() + lastName?: string; + + /** Admin UI single name field — split into first/last */ + @IsOptional() + @IsString() + name?: string; + + @IsString() + @MinLength(10) + phone!: string; + + @IsOptional() + @IsString() + nationalId?: string; + + @IsOptional() + @IsEnum(CustomerType) + customerType?: CustomerType; + + @IsOptional() + @IsString() + companyId?: string | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + levelDefId?: string | null; + + @IsOptional() + @IsString() + assignedAgentId?: string | null; + + @IsOptional() + @IsEnum(AppUserRole) + appRole?: AppUserRole; + + @IsOptional() + @IsString() + agentCode?: string | null; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + brandIds?: string[]; +} + +export class UpdateCustomerDto { + @IsOptional() + @IsString() + firstName?: string; + + @IsOptional() + @IsString() + lastName?: string; + + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + phone?: string; + + @IsOptional() + @IsString() + nationalId?: string | null; + + @IsOptional() + @IsEnum(CustomerType) + customerType?: CustomerType; + + @IsOptional() + @IsString() + companyId?: string | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + levelDefId?: string | null; + + @IsOptional() + @IsString() + assignedAgentId?: string | null; + + @IsOptional() + @IsEnum(AppUserRole) + appRole?: AppUserRole; + + @IsOptional() + @IsString() + agentCode?: string | null; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + brandIds?: string[]; +} + +export class SetCustomerActiveDto { + @IsBoolean() + isActive!: boolean; +} diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts new file mode 100644 index 0000000..79c2afe --- /dev/null +++ b/src/dashboard/dashboard.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get } from '@nestjs/common'; +import { DashboardService } from './dashboard.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; + +@Controller('admin') +@AuthType('admin') +@Roles('admin', 'sales', 'finance') +export class DashboardController { + constructor(private readonly dashboardService: DashboardService) {} + + @Get('dashboard') + getDashboard() { + return this.dashboardService.getDashboard(); + } + + @Get('reports') + getReports() { + return this.dashboardService.getReports(); + } +} diff --git a/src/dashboard/dashboard.module.ts b/src/dashboard/dashboard.module.ts new file mode 100644 index 0000000..6ba92f7 --- /dev/null +++ b/src/dashboard/dashboard.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DashboardController } from './dashboard.controller'; +import { DashboardService } from './dashboard.service'; +import { OrdersModule } from '../orders/orders.module'; + +@Module({ + imports: [OrdersModule], + controllers: [DashboardController], + providers: [DashboardService], +}) +export class DashboardModule {} diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts new file mode 100644 index 0000000..54c5c81 --- /dev/null +++ b/src/dashboard/dashboard.service.ts @@ -0,0 +1,394 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { + ADMIN_STATUS_MAP, + ORDER_STATUS_LABELS, + toAdminStatus, +} from '../common/utils/order-status'; +import { OrdersService } from '../orders/orders.service'; + +@Injectable() +export class DashboardService { + constructor( + private readonly prisma: PrismaService, + private readonly ordersService: OrdersService, + ) {} + + async getDashboard() { + const now = new Date(); + const startOfToday = new Date(now); + startOfToday.setHours(0, 0, 0, 0); + const startOfYesterday = new Date(startOfToday); + startOfYesterday.setDate(startOfYesterday.getDate() - 1); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startOfChart = new Date(startOfToday); + startOfChart.setDate(startOfChart.getDate() - 27); + + const [ + newOrders, + pendingReview, + pendingCheck, + finalApproval, + todayOrders, + yesterdayOrders, + monthOrders, + chartOrders, + recentOrders, + checks, + notifications, + messages, + ] = await Promise.all([ + this.prisma.order.count({ + where: { createdAt: { gte: startOfToday } }, + }), + this.prisma.order.count({ + where: { status: { in: ADMIN_STATUS_MAP.pending_review } }, + }), + this.prisma.order.count({ + where: { status: { in: ADMIN_STATUS_MAP.pending_check } }, + }), + this.prisma.order.count({ + where: { status: { in: ADMIN_STATUS_MAP.final_approval } }, + }), + this.prisma.order.findMany({ + where: { createdAt: { gte: startOfToday } }, + select: { finalAmount: true, totalAmount: true }, + }), + this.prisma.order.findMany({ + where: { + createdAt: { gte: startOfYesterday, lt: startOfToday }, + }, + select: { finalAmount: true, totalAmount: true }, + }), + this.prisma.order.findMany({ + where: { createdAt: { gte: startOfMonth } }, + select: { finalAmount: true, totalAmount: true }, + }), + this.prisma.order.findMany({ + where: { createdAt: { gte: startOfChart } }, + select: { + createdAt: true, + finalAmount: true, + totalAmount: true, + }, + }), + this.prisma.order.findMany({ + orderBy: { createdAt: 'desc' }, + take: 5, + include: { + items: true, + checks: { orderBy: { row: 'asc' } }, + cashReceipt: true, + statusHistory: { orderBy: { changedAt: 'asc' } }, + shipment: true, + user: true, + company: true, + }, + }), + this.prisma.check.findMany({ + where: { + status: { not: 'rejected' }, + dueDate: { + gte: startOfToday, + lte: new Date(startOfToday.getTime() + 30 * 24 * 60 * 60 * 1000), + }, + }, + select: { dueDate: true, amount: true }, + }), + this.prisma.adminNotification.findMany({ + orderBy: { sentAt: 'desc' }, + take: 20, + }), + this.prisma.internalMessage.findMany({ + orderBy: { sentAt: 'desc' }, + take: 20, + }), + ]); + + const sumAmounts = ( + rows: { finalAmount: number | null; totalAmount: number | null }[], + ) => rows.reduce((s, o) => s + (o.finalAmount ?? o.totalAmount ?? 0), 0); + + const todaySales = sumAmounts(todayOrders); + const yesterdaySales = sumAmounts(yesterdayOrders); + const monthSales = sumAmounts(monthOrders); + const todaySalesGrowth = + yesterdaySales > 0 + ? Math.round(((todaySales - yesterdaySales) / yesterdaySales) * 100) + : todaySales > 0 + ? 100 + : 0; + + const salesByDay = Array.from({ length: 28 }, () => 0); + for (const order of chartOrders) { + const dayIndex = Math.floor( + (order.createdAt.getTime() - startOfChart.getTime()) / + (24 * 60 * 60 * 1000), + ); + if (dayIndex >= 0 && dayIndex < 28) { + salesByDay[dayIndex] += order.finalAmount ?? order.totalAmount ?? 0; + } + } + const maxSale = Math.max(...salesByDay, 1); + const salesChartData = salesByDay.map((v) => + Number((v / maxSale).toFixed(3)), + ); + + const checkDeadlines = this.buildCheckDeadlines(checks, startOfToday); + + return { + stats: { + newOrders, + pendingReview, + pendingCheck, + finalApproval, + todaySales, + todaySalesGrowth, + monthSales, + }, + recentOrders: recentOrders.map((o) => + this.ordersService.mapOrder(o, true), + ), + salesChartData, + chartAxisLabels: ['۱', '۷', '۱۴', '۲۱', '۲۸'], + checkDeadlines, + notifications: notifications.map((n) => ({ + id: n.id, + title: n.title, + body: n.body, + tone: n.tone, + link: n.link, + isRead: n.isRead, + sentAt: n.sentAt.toISOString(), + })), + messages: messages.map((m) => ({ + id: m.id, + senderName: m.senderName, + subject: m.subject, + preview: m.preview, + isRead: m.isRead, + sentAt: m.sentAt.toISOString(), + })), + }; + } + + async getReports() { + const orders = await this.prisma.order.findMany({ + select: { + companyName: true, + status: true, + totalMeterage: true, + finalAmount: true, + totalAmount: true, + userId: true, + createdAt: true, + user: { + select: { firstName: true, lastName: true }, + }, + items: { + select: { + productId: true, + productName: true, + productCode: true, + requestedMeterage: true, + lineTotal: true, + }, + }, + }, + }); + + const customers = await this.prisma.customer.findMany({ + select: { + id: true, + firstName: true, + lastName: true, + phone: true, + isActive: true, + createdAt: true, + assignedAgentId: true, + orders: { + select: { + createdAt: true, + finalAmount: true, + totalAmount: true, + totalMeterage: true, + }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, + _count: { select: { orders: true } }, + }, + }); + + const byCompany = new Map< + string, + { + companyName: string; + orderCount: number; + totalAmount: number; + meterage: number; + } + >(); + const byStatus = new Map(); + const byMonth = new Map< + string, + { month: string; orderCount: number; totalAmount: number; meterage: number } + >(); + const byProduct = new Map< + string, + { + productId: string; + productName: string; + productCode: string; + orderCount: number; + meterage: number; + totalAmount: number; + } + >(); + const byCustomer = new Map< + string, + { + customerName: string; + orderCount: number; + totalAmount: number; + meterage: number; + } + >(); + + for (const order of orders) { + const amount = order.finalAmount ?? order.totalAmount ?? 0; + const company = byCompany.get(order.companyName) ?? { + companyName: order.companyName, + orderCount: 0, + totalAmount: 0, + meterage: 0, + }; + company.orderCount += 1; + company.totalAmount += amount; + company.meterage += order.totalMeterage; + byCompany.set(order.companyName, company); + + const label = + ORDER_STATUS_LABELS[order.status] ?? toAdminStatus(order.status); + byStatus.set(label, (byStatus.get(label) ?? 0) + 1); + + const monthKey = `${order.createdAt.getFullYear()}-${String(order.createdAt.getMonth() + 1).padStart(2, '0')}`; + const monthRow = byMonth.get(monthKey) ?? { + month: monthKey, + orderCount: 0, + totalAmount: 0, + meterage: 0, + }; + monthRow.orderCount += 1; + monthRow.totalAmount += amount; + monthRow.meterage += order.totalMeterage; + byMonth.set(monthKey, monthRow); + + const customerName = + `${order.user.firstName} ${order.user.lastName}`.trim() || order.userId; + const customerRow = byCustomer.get(customerName) ?? { + customerName, + orderCount: 0, + totalAmount: 0, + meterage: 0, + }; + customerRow.orderCount += 1; + customerRow.totalAmount += amount; + customerRow.meterage += order.totalMeterage; + byCustomer.set(customerName, customerRow); + + for (const item of order.items) { + const key = item.productId ?? `${item.productName}-${item.productCode}`; + const product = byProduct.get(key) ?? { + productId: item.productId ?? key, + productName: item.productName, + productCode: item.productCode ?? '', + orderCount: 0, + meterage: 0, + totalAmount: 0, + }; + product.orderCount += 1; + product.meterage += item.requestedMeterage; + product.totalAmount += item.lineTotal ?? 0; + byProduct.set(key, product); + } + } + + const now = new Date(); + const days30 = new Date(now); + days30.setDate(days30.getDate() - 30); + const days90 = new Date(now); + days90.setDate(days90.getDate() - 90); + + const reportCustomers = { + active: customers.filter((c) => c.isActive).length, + inactive: customers.filter((c) => !c.isActive).length, + newLast30Days: customers.filter((c) => c.createdAt >= days30).length, + noPurchase: customers.filter((c) => c._count.orders === 0).length, + noPurchase90Days: customers.filter((c) => { + const last = c.orders[0]?.createdAt; + return !last || last < days90; + }).length, + withAgent: customers.filter((c) => !!c.assignedAgentId).length, + }; + + return { + reportSalesByCompany: Array.from(byCompany.values()).sort( + (a, b) => b.totalAmount - a.totalAmount, + ), + reportOrdersByStatus: Array.from(byStatus.entries()).map( + ([status, count]) => ({ status, count }), + ), + reportSalesByMonth: Array.from(byMonth.values()).sort((a, b) => + a.month < b.month ? 1 : -1, + ), + reportSalesByProduct: Array.from(byProduct.values()) + .sort((a, b) => b.meterage - a.meterage) + .slice(0, 50), + reportSalesByCustomer: Array.from(byCustomer.values()) + .sort((a, b) => b.totalAmount - a.totalAmount) + .slice(0, 50), + reportCustomers, + }; + } + + private buildCheckDeadlines( + checks: { dueDate: Date; amount: number }[], + startOfToday: Date, + ) { + const buckets = [ + { id: 'cd-today', label: 'امروز', tone: 'danger' as const, days: 0 }, + { id: 'cd-3', label: '۳ روز آینده', tone: 'warning' as const, days: 3 }, + { id: 'cd-7', label: '۷ روز آینده', tone: 'warning' as const, days: 7 }, + { + id: 'cd-30', + label: '۳۰ روز آینده', + tone: 'neutral' as const, + days: 30, + }, + ]; + + return buckets.map((bucket) => { + const end = new Date(startOfToday); + end.setDate(end.getDate() + bucket.days); + end.setHours(23, 59, 59, 999); + + const matched = checks.filter((c) => { + const d = new Date(c.dueDate); + d.setHours(0, 0, 0, 0); + if (bucket.days === 0) { + return d.getTime() === startOfToday.getTime(); + } + return d >= startOfToday && d <= end; + }); + + return { + id: bucket.id, + label: bucket.label, + tone: bucket.tone, + count: matched.length, + amount: matched.reduce((s, c) => s + c.amount, 0), + }; + }); + } +} diff --git a/src/levels/dto/level.dto.ts b/src/levels/dto/level.dto.ts new file mode 100644 index 0000000..94272e3 --- /dev/null +++ b/src/levels/dto/level.dto.ts @@ -0,0 +1,81 @@ +import { + IsArray, + IsBoolean, + IsInt, + IsOptional, + IsString, + Min, + MinLength, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class LevelBenefitInputDto { + @IsString() + @MinLength(1) + title!: string; + + @IsOptional() + @IsString() + description?: string; +} + +export class CreateLevelDto { + @IsString() + @MinLength(1) + code!: string; + + @IsString() + @MinLength(1) + label!: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + sortOrder?: number; + + @IsOptional() + criteriaJson?: unknown; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => LevelBenefitInputDto) + benefits?: LevelBenefitInputDto[]; +} + +export class UpdateLevelDto { + @IsOptional() + @IsString() + @MinLength(1) + code?: string; + + @IsOptional() + @IsString() + @MinLength(1) + label?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + sortOrder?: number; + + @IsOptional() + criteriaJson?: unknown; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => LevelBenefitInputDto) + benefits?: LevelBenefitInputDto[]; +} diff --git a/src/levels/levels.controller.ts b/src/levels/levels.controller.ts new file mode 100644 index 0000000..d6bccaf --- /dev/null +++ b/src/levels/levels.controller.ts @@ -0,0 +1,25 @@ +import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; +import { LevelsService } from './levels.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CreateLevelDto, UpdateLevelDto } from './dto/level.dto'; + +@Controller('admin/levels') +@AuthType('admin') +export class LevelsController { + constructor(private readonly levelsService: LevelsService) {} + + @Get() + list() { + return this.levelsService.list(); + } + + @Post() + create(@Body() dto: CreateLevelDto) { + return this.levelsService.create(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateLevelDto) { + return this.levelsService.update(id, dto); + } +} diff --git a/src/levels/levels.module.ts b/src/levels/levels.module.ts new file mode 100644 index 0000000..8119969 --- /dev/null +++ b/src/levels/levels.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { LevelsController } from './levels.controller'; +import { LevelsService } from './levels.service'; + +@Module({ + controllers: [LevelsController], + providers: [LevelsService], + exports: [LevelsService], +}) +export class LevelsModule {} diff --git a/src/levels/levels.service.ts b/src/levels/levels.service.ts new file mode 100644 index 0000000..17ed6a2 --- /dev/null +++ b/src/levels/levels.service.ts @@ -0,0 +1,108 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateLevelDto, UpdateLevelDto } from './dto/level.dto'; + +@Injectable() +export class LevelsService { + constructor(private readonly prisma: PrismaService) {} + + async list() { + const rows = await this.prisma.customerLevelDef.findMany({ + orderBy: [{ sortOrder: 'asc' }, { label: 'asc' }], + include: { benefits: true, _count: { select: { customers: true } } }, + }); + return rows.map((r) => this.map(r)); + } + + async create(dto: CreateLevelDto) { + const row = await this.prisma.customerLevelDef.create({ + data: { + code: dto.code.trim(), + label: dto.label.trim(), + sortOrder: dto.sortOrder ?? 0, + criteriaJson: + dto.criteriaJson === undefined + ? undefined + : (dto.criteriaJson as Prisma.InputJsonValue), + isActive: dto.isActive ?? true, + benefits: dto.benefits?.length + ? { + create: dto.benefits.map((b) => ({ + title: b.title.trim(), + description: b.description?.trim() || null, + })), + } + : undefined, + }, + include: { benefits: true, _count: { select: { customers: true } } }, + }); + return this.map(row); + } + + async update(id: string, dto: UpdateLevelDto) { + await this.ensure(id); + if (dto.benefits) { + await this.prisma.levelBenefit.deleteMany({ where: { levelId: id } }); + } + const row = await this.prisma.customerLevelDef.update({ + where: { id }, + data: { + code: dto.code?.trim(), + label: dto.label?.trim(), + sortOrder: dto.sortOrder, + criteriaJson: + dto.criteriaJson === undefined + ? undefined + : (dto.criteriaJson as Prisma.InputJsonValue), + isActive: dto.isActive, + benefits: dto.benefits + ? { + create: dto.benefits.map((b) => ({ + title: b.title.trim(), + description: b.description?.trim() || null, + })), + } + : undefined, + }, + include: { benefits: true, _count: { select: { customers: true } } }, + }); + return this.map(row); + } + + private async ensure(id: string) { + const row = await this.prisma.customerLevelDef.findUnique({ where: { id } }); + if (!row) throw new NotFoundException('سطح یافت نشد'); + return row; + } + + private map(r: { + id: string; + code: string; + label: string; + sortOrder: number; + criteriaJson: unknown; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + benefits: Array<{ id: string; title: string; description: string | null }>; + _count?: { customers: number }; + }) { + return { + id: r.id, + code: r.code, + label: r.label, + sortOrder: r.sortOrder, + criteriaJson: r.criteriaJson ?? undefined, + isActive: r.isActive, + customersCount: r._count?.customers ?? 0, + benefits: r.benefits.map((b) => ({ + id: b.id, + title: b.title, + description: b.description ?? undefined, + })), + createdAt: r.createdAt.toISOString(), + updatedAt: r.updatedAt.toISOString(), + }; + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..80a921f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,47 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { json, urlencoded } from 'express'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // تصاویر چک و رسید به‌صورت base64 داخل JSON می‌آیند و از سقف پیش‌فرض ۱۰۰KB رد می‌شوند + const bodyLimit = process.env.BODY_LIMIT || '25mb'; + app.use(json({ limit: bodyLimit })); + app.use(urlencoded({ extended: true, limit: bodyLimit })); + + // پشت LiteSpeed روی /server → GLOBAL_PREFIX=server/api + const globalPrefix = process.env.GLOBAL_PREFIX || 'api'; + app.setGlobalPrefix(globalPrefix); + app.enableCors({ + origin: true, + credentials: true, + }); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + transformOptions: { enableImplicitConversion: true }, + }), + ); + + const swaggerConfig = new DocumentBuilder() + .setTitle('Fabric Trade API') + .setDescription('API سرور تجارت پارچه') + .setVersion('1.0') + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup(`${globalPrefix}/docs`, app, document); + + const port = process.env.PORT ?? 3000; + await app.listen(port); + console.log( + `Fabric Trade API listening on http://localhost:${port}/${globalPrefix}`, + ); + console.log(`Swagger docs at http://localhost:${port}/${globalPrefix}/docs`); +} +void bootstrap(); diff --git a/src/notifications/notification-events.ts b/src/notifications/notification-events.ts new file mode 100644 index 0000000..3055ae3 --- /dev/null +++ b/src/notifications/notification-events.ts @@ -0,0 +1,193 @@ +import { NotificationTone, OrderStatus } from '@prisma/client'; + +/** رویدادهایی که برای مشتری اعلان و پیامک تولید می‌کنند. */ +export type CustomerEvent = + | 'order_submitted' + | 'invoice_priced' + | 'checks_approved' + | 'checks_need_fix' + | 'receipt_approved' + | 'receipt_rejected' + | 'cash_invoice_reminder' + | 'cash_invoice_expired' + | 'order_preparing' + | 'order_ready_to_ship' + | 'order_shipped' + | 'order_delivered' + | 'order_cancelled'; + +/** کلید سوئیچ‌های پیامک در SystemSettings؛ null یعنی پیامک ندارد. */ +type SmsSwitch = + | 'smsOrderRegistered' + | 'smsPricingReady' + | 'smsPaymentReminder' + | 'smsCheckReview' + | 'smsShippingUpdate' + | null; + +export type CustomerEventTemplate = { + title: string; + /** متن اعلان درون‌برنامه‌ای */ + body: (ctx: EventContext) => string; + /** متن پیامک؛ نبودنش یعنی فقط اعلان درون‌برنامه‌ای */ + sms?: (ctx: EventContext) => string; + tone: NotificationTone; + smsSwitch: SmsSwitch; +}; + +export type EventContext = { + orderNumber: string; + appName: string; + amount?: number | null; + note?: string | null; + deadline?: string | null; + carrier?: string | null; + trackingNumber?: string | null; +}; + +function money(amount?: number | null): string { + if (!amount) return '—'; + return `${Math.round(amount).toLocaleString('fa-IR')} تومان`; +} + +/** + * وضعیت‌هایی که به‌تنهایی برای مشتری خبر دارند. وضعیت‌های میانی مثل + * awaiting_check_review خبری برای مشتری ندارند و اینجا نمی‌آیند. + */ +export function eventForStatus(status: OrderStatus): CustomerEvent | null { + switch (status) { + case OrderStatus.awaiting_settlement: + return 'invoice_priced'; + case OrderStatus.preparing: + case OrderStatus.final_approved: + return 'order_preparing'; + case OrderStatus.ready_to_ship: + return 'order_ready_to_ship'; + case OrderStatus.shipped: + return 'order_shipped'; + case OrderStatus.delivered: + return 'order_delivered'; + case OrderStatus.cancelled: + case OrderStatus.rejected: + return 'order_cancelled'; + case OrderStatus.expired: + return 'cash_invoice_expired'; + default: + return null; + } +} + +export const CUSTOMER_EVENTS: Record = { + order_submitted: { + title: 'سفارش شما ثبت شد', + body: (c) => + `سفارش ${c.orderNumber} ثبت شد و برای بررسی و قیمت‌گذاری به کارشناسان ما رسید.`, + sms: (c) => + `سفارش ${c.orderNumber} در ${c.appName} ثبت شد. پس از قیمت‌گذاری، فاکتور برای شما ارسال می‌شود.`, + tone: NotificationTone.info, + smsSwitch: 'smsOrderRegistered', + }, + invoice_priced: { + title: 'فاکتور سفارش شما صادر شد', + body: (c) => + `مبلغ فاکتور سفارش ${c.orderNumber} ${money(c.amount)} است. برای ادامه، روش تسویه را انتخاب کنید.`, + sms: (c) => + `فاکتور سفارش ${c.orderNumber} به مبلغ ${money(c.amount)} صادر شد. برای انتخاب روش تسویه به پنل ${c.appName} مراجعه کنید.`, + tone: NotificationTone.success, + smsSwitch: 'smsPricingReady', + }, + checks_approved: { + title: 'چک‌های شما تایید شد', + body: (c) => `همه چک‌های سفارش ${c.orderNumber} تایید شدند.`, + sms: (c) => + `چک‌های سفارش ${c.orderNumber} تایید شد و سفارش به مرحله آماده‌سازی می‌رود.`, + tone: NotificationTone.success, + smsSwitch: 'smsCheckReview', + }, + checks_need_fix: { + title: 'چک شما نیاز به اصلاح دارد', + body: (c) => + `یک یا چند چک سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً چک‌ها را اصلاح و مجدداً ارسال کنید.`, + sms: (c) => + `چک سفارش ${c.orderNumber} تایید نشد. برای اصلاح چک‌ها به پنل ${c.appName} مراجعه کنید.`, + tone: NotificationTone.danger, + smsSwitch: 'smsCheckReview', + }, + receipt_approved: { + title: 'پرداخت شما تایید شد', + body: (c) => `رسید پرداخت سفارش ${c.orderNumber} تایید شد.`, + sms: (c) => + `رسید پرداخت سفارش ${c.orderNumber} تایید شد. سفارش شما در حال آماده‌سازی است.`, + tone: NotificationTone.success, + smsSwitch: 'smsPaymentReminder', + }, + receipt_rejected: { + title: 'رسید پرداخت تایید نشد', + body: (c) => + `رسید پرداخت سفارش ${c.orderNumber} تایید نشد${c.note ? `: ${c.note}` : '.'} لطفاً رسید صحیح را ثبت کنید.`, + sms: (c) => + `رسید پرداخت سفارش ${c.orderNumber} تایید نشد. لطفاً در پنل ${c.appName} رسید را اصلاح کنید.`, + tone: NotificationTone.danger, + smsSwitch: 'smsPaymentReminder', + }, + cash_invoice_reminder: { + title: 'یادآوری مهلت پرداخت', + body: (c) => + `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان نزدیک است. مبلغ قابل پرداخت ${money(c.amount)}.`, + sms: (c) => + `مهلت پرداخت فاکتور سفارش ${c.orderNumber} در حال پایان است. مبلغ ${money(c.amount)}.`, + tone: NotificationTone.warning, + smsSwitch: 'smsPaymentReminder', + }, + cash_invoice_expired: { + title: 'مهلت پرداخت فاکتور تمام شد', + body: (c) => + `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید. برای ثبت سفارش تازه با پشتیبانی تماس بگیرید.`, + sms: (c) => `مهلت پرداخت فاکتور سفارش ${c.orderNumber} به پایان رسید.`, + tone: NotificationTone.danger, + smsSwitch: 'smsPaymentReminder', + }, + order_preparing: { + title: 'سفارش در حال آماده‌سازی', + body: (c) => + `سفارش ${c.orderNumber} تایید نهایی شد و در حال آماده‌سازی است.`, + tone: NotificationTone.info, + smsSwitch: null, + }, + order_ready_to_ship: { + title: 'سفارش آماده ارسال است', + body: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`, + sms: (c) => `سفارش ${c.orderNumber} آماده ارسال شد.`, + tone: NotificationTone.info, + smsSwitch: 'smsShippingUpdate', + }, + order_shipped: { + title: 'سفارش ارسال شد', + body: (c) => + `سفارش ${c.orderNumber} ارسال شد${c.carrier ? ` (${c.carrier})` : ''}${ + c.trackingNumber ? ` با کد رهگیری ${c.trackingNumber}` : '' + }.`, + sms: (c) => + `سفارش ${c.orderNumber} ارسال شد${ + c.trackingNumber ? `. کد رهگیری: ${c.trackingNumber}` : '.' + }`, + tone: NotificationTone.success, + smsSwitch: 'smsShippingUpdate', + }, + order_delivered: { + title: 'سفارش تحویل شد', + body: (c) => + `سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`, + sms: (c) => `سفارش ${c.orderNumber} تحویل داده شد. از خرید شما سپاسگزاریم.`, + tone: NotificationTone.success, + smsSwitch: 'smsShippingUpdate', + }, + order_cancelled: { + title: 'سفارش لغو شد', + body: (c) => + `سفارش ${c.orderNumber} لغو شد${c.note ? `: ${c.note}` : '.'} برای اطلاعات بیشتر با پشتیبانی تماس بگیرید.`, + sms: (c) => `سفارش ${c.orderNumber} لغو شد.`, + tone: NotificationTone.danger, + smsSwitch: null, + }, +}; diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..56cc65b --- /dev/null +++ b/src/notifications/notifications.controller.ts @@ -0,0 +1,81 @@ +import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; +import { NotificationTone } from '@prisma/client'; +import { + ArrayNotEmpty, + IsArray, + IsEnum, + IsOptional, + IsString, + MinLength, +} from 'class-validator'; +import { NotificationsService } from './notifications.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; + +@Controller('notifications') +@AuthType('customer') +export class CustomerNotificationsController { + constructor(private readonly notifications: NotificationsService) {} + + @Get() + list(@CurrentUser() user: AuthPrincipal) { + return this.notifications.listForCustomer(user.id); + } + + @Get('unread-count') + unreadCount(@CurrentUser() user: AuthPrincipal) { + return this.notifications.unreadCount(user.id); + } + + @Patch('read-all') + markAllRead(@CurrentUser() user: AuthPrincipal) { + return this.notifications.markAllRead(user.id); + } + + @Patch(':id/read') + markRead(@CurrentUser() user: AuthPrincipal, @Param('id') id: string) { + return this.notifications.markRead(user.id, id); + } +} + +class BroadcastDto { + @IsString() + @MinLength(1) + title!: string; + + @IsString() + @MinLength(1) + body!: string; + + @IsOptional() + @IsArray() + @ArrayNotEmpty() + @IsString({ each: true }) + customerIds?: string[]; + + @IsOptional() + @IsEnum(NotificationTone) + tone?: NotificationTone; +} + +@Controller('admin/notifications') +@AuthType('admin') +export class AdminNotificationsController { + constructor(private readonly notifications: NotificationsService) {} + + @Post('broadcast') + broadcast(@Body() dto: BroadcastDto) { + return this.notifications.broadcastToCustomers(dto); + } + + @Patch('read-all') + markAllRead() { + return this.notifications.markAllAdminRead(); + } + + @Patch(':id/read') + markRead(@Param('id') id: string) { + return this.notifications.markAdminRead(id); + } +} diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts new file mode 100644 index 0000000..3f2933a --- /dev/null +++ b/src/notifications/notifications.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { NotificationsService } from './notifications.service'; +import { + AdminNotificationsController, + CustomerNotificationsController, +} from './notifications.controller'; +import { SmsModule } from '../sms/sms.module'; + +@Module({ + imports: [SmsModule], + controllers: [CustomerNotificationsController, AdminNotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], +}) +export class NotificationsModule {} diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts new file mode 100644 index 0000000..6bbd3d7 --- /dev/null +++ b/src/notifications/notifications.service.ts @@ -0,0 +1,251 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NotificationTone } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { SmsService } from '../sms/sms.service'; +import { + CUSTOMER_EVENTS, + type CustomerEvent, + type EventContext, +} from './notification-events'; + +type NotifyOptions = { + amount?: number | null; + note?: string | null; + carrier?: string | null; + trackingNumber?: string | null; +}; + +@Injectable() +export class NotificationsService { + private readonly logger = new Logger(NotificationsService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly sms: SmsService, + private readonly config: ConfigService, + ) {} + + /** + * باید **بعد از commit تراکنش** صدا زده شود؛ وگرنه با rollback هم پیامک رفته + * است و هم اعلانی برای سفارشی ساخته می‌شود که وجود ندارد. + */ + async notifyOrderEvent( + orderId: string, + event: CustomerEvent, + options: NotifyOptions = {}, + ): Promise { + try { + const order = await this.prisma.order.findUnique({ + where: { id: orderId }, + select: { + id: true, + orderNumber: true, + userId: true, + finalAmount: true, + totalAmount: true, + user: { select: { phone: true } }, + }, + }); + if (!order) return; + + const template = CUSTOMER_EVENTS[event]; + const ctx: EventContext = { + orderNumber: order.orderNumber, + appName: this.appName(), + amount: + options.amount ?? order.finalAmount ?? order.totalAmount ?? undefined, + note: options.note ?? null, + carrier: options.carrier ?? null, + trackingNumber: options.trackingNumber ?? null, + }; + + const notification = await this.prisma.customerNotification.create({ + data: { + customerId: order.userId, + orderId: order.id, + event, + title: template.title, + body: template.body(ctx), + tone: template.tone, + link: `/orders/${order.id}`, + }, + }); + + if (!template.sms || !template.smsSwitch) return; + const enabled = await this.isSmsEventEnabled(template.smsSwitch); + if (!enabled || !this.sms.isEnabled()) return; + + const result = await this.sms.send(order.user.phone, template.sms(ctx)); + if (result.success) { + await this.prisma.customerNotification.update({ + where: { id: notification.id }, + data: { smsSent: true }, + }); + } + } catch (err) { + // اعلان هیچ‌وقت نباید عملیات اصلی را بشکند + this.logger.error( + `notifyOrderEvent(${event}) failed for order ${orderId}`, + err instanceof Error ? err.stack : err, + ); + } + } + + /** نسخه بدون await برای مسیرهایی که نباید منتظر پیامک بمانند. */ + dispatchOrderEvent( + orderId: string, + event: CustomerEvent, + options: NotifyOptions = {}, + ): void { + void this.notifyOrderEvent(orderId, event, options); + } + + async notifyAdmin(input: { + title: string; + body: string; + tone?: NotificationTone; + link?: string; + }): Promise { + try { + await this.prisma.adminNotification.create({ + data: { + title: input.title, + body: input.body, + tone: input.tone ?? NotificationTone.info, + link: input.link ?? null, + }, + }); + } catch (err) { + this.logger.error( + 'notifyAdmin failed', + err instanceof Error ? err.stack : err, + ); + } + } + + dispatchAdmin(input: { + title: string; + body: string; + tone?: NotificationTone; + link?: string; + }): void { + void this.notifyAdmin(input); + } + + async listForCustomer(customerId: string, limit = 50) { + const rows = await this.prisma.customerNotification.findMany({ + where: { customerId }, + orderBy: { sentAt: 'desc' }, + take: limit, + }); + return rows.map((n) => ({ + id: n.id, + orderId: n.orderId ?? undefined, + event: n.event, + title: n.title, + body: n.body, + tone: n.tone ?? NotificationTone.info, + link: n.link ?? undefined, + isRead: n.isRead, + sentAt: n.sentAt.toISOString(), + })); + } + + async unreadCount(customerId: string) { + const count = await this.prisma.customerNotification.count({ + where: { customerId, isRead: false }, + }); + return { count }; + } + + async markRead(customerId: string, id: string) { + const existing = await this.prisma.customerNotification.findFirst({ + where: { id, customerId }, + select: { id: true }, + }); + if (!existing) throw new NotFoundException('اعلان یافت نشد'); + await this.prisma.customerNotification.update({ + where: { id }, + data: { isRead: true }, + }); + return this.unreadCount(customerId); + } + + async markAllRead(customerId: string) { + await this.prisma.customerNotification.updateMany({ + where: { customerId, isRead: false }, + data: { isRead: true }, + }); + return { count: 0 }; + } + + async markAdminRead(id: string) { + await this.prisma.adminNotification.updateMany({ + where: { id }, + data: { isRead: true }, + }); + return { ok: true }; + } + + async markAllAdminRead() { + await this.prisma.adminNotification.updateMany({ + where: { isRead: false }, + data: { isRead: true }, + }); + return { ok: true }; + } + + /** اعلان هدفمند: همه مشتریان فعال یا لیست مشخص (§۱۴) */ + async broadcastToCustomers(input: { + title: string; + body: string; + customerIds?: string[]; + tone?: NotificationTone; + }) { + const where = input.customerIds?.length + ? { id: { in: input.customerIds }, isActive: true } + : { isActive: true, appRole: 'customer' as const }; + const customers = await this.prisma.customer.findMany({ + where, + select: { id: true }, + }); + if (!customers.length) return { sent: 0 }; + await this.prisma.customerNotification.createMany({ + data: customers.map((c) => ({ + customerId: c.id, + event: 'admin_broadcast', + title: input.title.trim(), + body: input.body.trim(), + tone: input.tone ?? 'info', + })), + }); + return { sent: customers.length }; + } + + private appName(): string { + return this.config.get('SMS_APP_NAME', 'پارچه') ?? 'پارچه'; + } + + private async isSmsEventEnabled( + key: + | 'smsOrderRegistered' + | 'smsPricingReady' + | 'smsPaymentReminder' + | 'smsCheckReview' + | 'smsShippingUpdate', + ): Promise { + const settings = await this.prisma.systemSettings.findUnique({ + where: { id: 'default' }, + select: { + smsOrderRegistered: true, + smsPricingReady: true, + smsPaymentReminder: true, + smsCheckReview: true, + smsShippingUpdate: true, + }, + }); + // در نبود ردیف تنظیمات، پیش‌فرض‌های اسکیمای فعال در نظر گرفته می‌شوند + return settings ? settings[key] : true; + } +} diff --git a/src/orders/cash-invoice-scheduler.service.ts b/src/orders/cash-invoice-scheduler.service.ts new file mode 100644 index 0000000..7452bc2 --- /dev/null +++ b/src/orders/cash-invoice-scheduler.service.ts @@ -0,0 +1,120 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { OrderStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { NotificationsService } from '../notifications/notifications.service'; + +/** وضعیت‌هایی که فاکتور نقدی در آن‌ها منتظر پرداخت مشتری است. */ +const PAYABLE_STATUSES: OrderStatus[] = [ + OrderStatus.awaiting_cash_payment, + OrderStatus.cash_invoice_issued, +]; + +/** چقدر پیش از پایان مهلت، یادآوری فرستاده شود. */ +const REMINDER_WINDOW_MS = 60 * 60 * 1000; + +@Injectable() +export class CashInvoiceSchedulerService { + private readonly logger = new Logger(CashInvoiceSchedulerService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} + + @Cron(CronExpression.EVERY_10_MINUTES) + async handleCashInvoices(): Promise { + await this.sendReminders(); + await this.expireOverdueInvoices(); + } + + /** یادآوری یک‌بار برای هر فاکتوری که کمتر از یک ساعت به پایان مهلتش مانده. */ + async sendReminders(): Promise { + const now = new Date(); + const soon = new Date(now.getTime() + REMINDER_WINDOW_MS); + + try { + const orders = await this.prisma.order.findMany({ + where: { + status: { in: PAYABLE_STATUSES }, + cashReminderSentAt: null, + cashInvoiceExpiresAt: { gt: now, lte: soon }, + }, + select: { id: true, finalAmount: true }, + }); + + for (const order of orders) { + await this.prisma.order.update({ + where: { id: order.id }, + data: { cashReminderSentAt: now }, + }); + await this.notifications.notifyOrderEvent( + order.id, + 'cash_invoice_reminder', + { amount: order.finalAmount }, + ); + } + return orders.length; + } catch (err) { + this.logger.error( + 'sendReminders failed', + err instanceof Error ? err.stack : err, + ); + return 0; + } + } + + /** فاکتورهای نقدی که مهلتشان تمام شده به وضعیت منقضی می‌روند. */ + async expireOverdueInvoices(): Promise { + const now = new Date(); + + try { + const orders = await this.prisma.order.findMany({ + where: { + status: { in: PAYABLE_STATUSES }, + cashInvoiceExpiresAt: { lt: now }, + }, + select: { id: true, orderNumber: true, status: true }, + }); + + for (const order of orders) { + await this.prisma.$transaction([ + this.prisma.order.update({ + where: { id: order.id }, + data: { status: OrderStatus.expired }, + }), + this.prisma.orderStatusHistory.create({ + data: { + orderId: order.id, + fromStatus: order.status, + toStatus: OrderStatus.expired, + changedBy: 'system', + note: 'پایان مهلت پرداخت فاکتور نقدی', + }, + }), + ]); + + await this.notifications.notifyOrderEvent( + order.id, + 'cash_invoice_expired', + ); + this.notifications.dispatchAdmin({ + title: 'فاکتور نقدی منقضی شد', + body: `مهلت پرداخت سفارش ${order.orderNumber} به پایان رسید و سفارش منقضی شد.`, + link: '/orders', + }); + } + + if (orders.length) { + this.logger.log(`${orders.length} فاکتور نقدی منقضی شد`); + } + return orders.length; + } catch (err) { + this.logger.error( + 'expireOverdueInvoices failed', + err instanceof Error ? err.stack : err, + ); + return 0; + } + } +} diff --git a/src/orders/dto/order.dto.ts b/src/orders/dto/order.dto.ts new file mode 100644 index 0000000..b3b541b --- /dev/null +++ b/src/orders/dto/order.dto.ts @@ -0,0 +1,268 @@ +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsDateString, + IsEnum, + IsNumber, + IsOptional, + IsString, + Min, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { + CheckRegistrationMode, + OrderStatus, + SettlementType, +} from '@prisma/client'; + +export class SubmitOrderItemDto { + @IsString() + productId!: string; + + @Type(() => Number) + @IsNumber() + @Min(0.001) + meterage!: number; + + /** Alias پذیرفته‌شده از کلاینت‌های قدیمی؛ نادیده گرفته می‌شود */ + @IsOptional() + @Type(() => Number) + @IsNumber() + requestedMeterage?: number; + + @IsOptional() + @IsString() + notes?: string; +} + +export class SubmitOrderCheckDto { + @Type(() => Number) + @IsNumber() + @Min(1) + amount!: number; + + @IsDateString() + dueDate!: string; + + @IsOptional() + @IsString() + sayadId?: string; + + @IsOptional() + @IsString() + checkNumber?: string; + + @IsOptional() + @IsString() + ownerNationalId?: string; + + @IsOptional() + @IsString() + ownerFirstName?: string; + + @IsOptional() + @IsString() + ownerLastName?: string; + + @IsOptional() + @IsString() + bankName?: string; + + @IsOptional() + @IsString() + branchName?: string; + + @IsOptional() + @IsString() + frontImage?: string; + + @IsOptional() + @IsString() + backImage?: string; + + @IsOptional() + @IsString() + notes?: string; + + @IsOptional() + @IsEnum(CheckRegistrationMode) + mode?: CheckRegistrationMode; +} + +export class SubmitOrderDto { + @IsString() + companyId!: string; + + /** + * روش تسویه بعد از قیمت‌گذاری ادمین انتخاب می‌شود، نه اینجا. + * برای سازگاری با کلاینت‌های قدیمی پذیرفته و نادیده گرفته می‌شود. + */ + @IsOptional() + @IsEnum(SettlementType) + settlementType?: SettlementType; + + @IsOptional() + @IsEnum(SettlementType) + settlementMethod?: SettlementType; + + @IsOptional() + @IsEnum(CheckRegistrationMode) + checkMode?: CheckRegistrationMode; + + @IsOptional() + @IsString() + notes?: string; + + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => SubmitOrderItemDto) + items!: SubmitOrderItemDto[]; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SubmitOrderCheckDto) + checks?: SubmitOrderCheckDto[]; +} + +export class ChooseSettlementDto { + @IsEnum(SettlementType) + settlementType!: SettlementType; + + @IsOptional() + @IsEnum(CheckRegistrationMode) + checkMode?: CheckRegistrationMode; +} + +export class AdminCreateOrderDto { + /** Customer / user id — preferred when picking from customers list */ + @IsOptional() + @IsString() + customerId?: string; + + @IsOptional() + @IsString() + userId?: string; + + @IsOptional() + @IsString() + customerPhone?: string; + + @IsOptional() + @IsString() + customerName?: string; + + @IsString() + companyId!: string; + + @IsOptional() + @IsEnum(SettlementType) + settlementType?: SettlementType; + + /** Alias for admin UI */ + @IsOptional() + @IsEnum(SettlementType) + settlementMethod?: SettlementType; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + meterage?: number; + + @IsOptional() + @IsString() + notes?: string; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SubmitOrderItemDto) + items?: SubmitOrderItemDto[]; +} + +export class OrderItemPricingDto { + @IsString() + id!: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + confirmedMeterage?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + unitPrice?: number; + + /** Alias from admin UI */ + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + supplyPrice?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + unitDiscount?: number; +} + +export class UpdateOrderPricingDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OrderItemPricingDto) + items!: OrderItemPricingDto[]; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + shippingCost?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + totalDiscount?: number; + + @IsOptional() + @IsString() + note?: string; +} + +export class UpdateOrderStatusDto { + @IsEnum(OrderStatus) + status!: OrderStatus; + + @IsOptional() + @IsString() + note?: string; + + @IsOptional() + @IsString() + reason?: string; + + /** عبور از ماشین وضعیت؛ فقط با ذکر دلیل و با ثبت در تاریخچه. */ + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + force?: boolean; +} + +export class UpdateOrderChecksDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SubmitOrderCheckDto) + checks!: SubmitOrderCheckDto[]; + + @IsOptional() + @IsEnum(CheckRegistrationMode) + checkMode?: CheckRegistrationMode; +} diff --git a/src/orders/orders.controller.ts b/src/orders/orders.controller.ts new file mode 100644 index 0000000..b608b1e --- /dev/null +++ b/src/orders/orders.controller.ts @@ -0,0 +1,104 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { OrdersService } from './orders.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + AdminCreateOrderDto, + ChooseSettlementDto, + SubmitOrderDto, + UpdateOrderChecksDto, + UpdateOrderPricingDto, + UpdateOrderStatusDto, +} from './dto/order.dto'; + +@Controller('orders') +export class CustomerOrdersController { + constructor(private readonly ordersService: OrdersService) {} + + @AuthType('customer') + @Get() + findMine(@CurrentUser() user: AuthPrincipal) { + return this.ordersService.findMyOrders(user.id); + } + + @AuthType('customer') + @Get(':id') + findOne(@CurrentUser() user: AuthPrincipal, @Param('id') id: string) { + return this.ordersService.findMyOrder(user.id, id); + } + + @AuthType('customer') + @Post() + submit(@CurrentUser() user: AuthPrincipal, @Body() dto: SubmitOrderDto) { + return this.ordersService.submitOrder(user.id, dto); + } + + @AuthType('customer') + @Post(':id/settlement') + chooseSettlement( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: ChooseSettlementDto, + ) { + return this.ordersService.chooseSettlement(user.id, id, dto); + } + + @AuthType('customer') + @Patch(':id/checks') + updateChecks( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: UpdateOrderChecksDto, + ) { + return this.ordersService.updateChecks(user.id, id, dto); + } +} + +@Controller('admin/orders') +@AuthType('admin') +@Roles('admin', 'sales', 'finance') +export class AdminOrdersController { + constructor(private readonly ordersService: OrdersService) {} + + @Get() + findAll(@Query('status') status?: string, @Query('q') q?: string) { + return this.ordersService.findAdminOrders({ status, q }); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.ordersService.findAdminOrder(id); + } + + @Post() + create(@CurrentUser() user: AuthPrincipal, @Body() dto: AdminCreateOrderDto) { + return this.ordersService.adminCreate(user.id, dto); + } + + @Patch(':id/pricing') + updatePricing( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: UpdateOrderPricingDto, + ) { + return this.ordersService.updatePricing(id, user.id, dto); + } + + @Patch(':id/status') + updateStatus( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: UpdateOrderStatusDto, + ) { + return this.ordersService.updateStatus(id, user.id, dto); + } +} diff --git a/src/orders/orders.module.ts b/src/orders/orders.module.ts new file mode 100644 index 0000000..6af1a26 --- /dev/null +++ b/src/orders/orders.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { + AdminOrdersController, + CustomerOrdersController, +} from './orders.controller'; +import { OrdersService } from './orders.service'; +import { CashInvoiceSchedulerService } from './cash-invoice-scheduler.service'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [NotificationsModule], + controllers: [CustomerOrdersController, AdminOrdersController], + providers: [OrdersService, CashInvoiceSchedulerService], + exports: [OrdersService], +}) +export class OrdersModule {} diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts new file mode 100644 index 0000000..6d9e286 --- /dev/null +++ b/src/orders/orders.service.ts @@ -0,0 +1,888 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { + CheckRegistrationMode, + OrderStatus, + Prisma, + SettlementType, +} from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + AdminCreateOrderDto, + ChooseSettlementDto, + SubmitOrderCheckDto, + SubmitOrderDto, + UpdateOrderChecksDto, + UpdateOrderPricingDto, + UpdateOrderStatusDto, +} from './dto/order.dto'; +import { generateOrderNumber } from '../common/utils/order-number'; +import { calculateCheckRass } from '../common/utils/check-rass'; +import { earliestCashInvoiceExpiry } from '../common/utils/cash-expiry'; +import { + resolveAdminStatusFilter, + toAdminStatus, +} from '../common/utils/order-status'; +import { + allowedTransitions, + assertTransition, +} from '../common/utils/order-transitions'; +import { resolveMaxMeterage } from '../common/utils/meterage'; +import { NotificationsService } from '../notifications/notifications.service'; +import { eventForStatus } from '../notifications/notification-events'; +import { joinFullName, splitFullName } from '../common/utils/name'; +import { + isValidPhone, + normalizePhone, + toEnglishDigits, +} from '../common/utils/phone'; + +const orderInclude = { + items: true, + checks: { orderBy: { row: 'asc' as const } }, + cashReceipt: true, + statusHistory: { orderBy: { changedAt: 'asc' as const } }, + shipment: true, + user: true, + company: true, +} satisfies Prisma.OrderInclude; + +type OrderFull = Prisma.OrderGetPayload<{ include: typeof orderInclude }>; + +/** مبالغ ریالی روی Float ذخیره می‌شوند؛ گرد کردن از انباشت خطای اعشاری جلوگیری می‌کند. */ +function roundMoney(value: number): number { + return Math.round(value * 100) / 100; +} + +/** بعد از تأیید چک‌ها یا ارسال سفارش، مشتری نباید بتواند چک‌ها را بازنویسی کند. */ +const CHECK_EDITABLE_STATUSES: OrderStatus[] = [ + OrderStatus.check_in_progress, + OrderStatus.awaiting_check_review, + OrderStatus.check_needs_fix, + OrderStatus.check_rejected, +]; + +/** + * تا وقتی ادمین فاکتور را نفرستاده، مشتری نباید هیچ مبلغی ببیند. + * پنهان‌سازی روی خود پاسخ API انجام می‌شود، نه در کلاینت. + */ +const PRICE_HIDDEN_STATUSES: OrderStatus[] = [ + OrderStatus.awaiting_review, + OrderStatus.pricing, +]; + +@Injectable() +export class OrdersService { + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} + + async findMyOrders(userId: string) { + const orders = await this.prisma.order.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + include: orderInclude, + }); + return orders.map((o) => this.mapOrder(o)); + } + + async findMyOrder(userId: string, id: string) { + const order = await this.getOrder(id); + if (order.userId !== userId) { + throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); + } + return this.mapOrder(order); + } + + async findAdminOrders(filters: { status?: string; q?: string }) { + const statuses = resolveAdminStatusFilter(filters.status); + const q = filters.q?.trim(); + const orders = await this.prisma.order.findMany({ + where: { + ...(statuses ? { status: { in: statuses } } : {}), + ...(q + ? { + OR: [ + { orderNumber: { contains: q, mode: 'insensitive' } }, + { companyName: { contains: q, mode: 'insensitive' } }, + { + user: { + OR: [ + { firstName: { contains: q, mode: 'insensitive' } }, + { lastName: { contains: q, mode: 'insensitive' } }, + { phone: { contains: toEnglishDigits(q) } }, + ], + }, + }, + ], + } + : {}), + }, + orderBy: { createdAt: 'desc' }, + include: orderInclude, + }); + return orders.map((o) => this.mapOrder(o, true)); + } + + async findAdminOrder(id: string) { + const order = await this.getOrder(id); + return this.mapOrder(order, true); + } + + async submitOrder(userId: string, dto: SubmitOrderDto) { + const settings = await this.prisma.systemSettings.findUnique({ + where: { id: 'default' }, + select: { + maxMeteragePerInvoice: true, + minOrderRollsPerProduct: true, + }, + }); + const minRolls = settings?.minOrderRollsPerProduct ?? 1; + + const productIds = dto.items.map((i) => i.productId); + const uniqueProductIds = [...new Set(productIds)]; + if (uniqueProductIds.length !== productIds.length) { + throw new BadRequestException('محصول تکراری در سفارش مجاز نیست'); + } + + const products = await this.prisma.product.findMany({ + where: { id: { in: uniqueProductIds } }, + include: { catalog: true, company: true }, + }); + const productMap = new Map(products.map((p) => [p.id, p])); + + for (const item of dto.items) { + const product = productMap.get(item.productId); + if (!product) { + throw new BadRequestException( + 'یک یا چند محصول نامعتبر است یا دیگر در سیستم وجود ندارد. سبد را خالی کنید و دوباره اضافه کنید.', + ); + } + if (!product.isActive) { + throw new BadRequestException( + `محصول «${product.name}» غیرفعال است و قابل سفارش نیست`, + ); + } + if (!product.company?.isActive) { + throw new BadRequestException( + `شرکت محصول «${product.name}» غیرفعال است`, + ); + } + } + + // سقف متراژ برای هر شرکت جداگانه کنترل می‌شود + const meterageByCompany = new Map< + string, + { name: string; meterage: number; max: number | null } + >(); + for (const item of dto.items) { + const product = productMap.get(item.productId)!; + const current = meterageByCompany.get(product.companyId); + if (current) { + current.meterage += item.meterage; + } else { + meterageByCompany.set(product.companyId, { + name: product.company.name, + meterage: item.meterage, + max: product.company.maxMeteragePerInvoice, + }); + } + } + + for (const row of meterageByCompany.values()) { + const maxMeterageResolved = resolveMaxMeterage( + row.max, + settings?.maxMeteragePerInvoice, + ); + if (row.meterage > maxMeterageResolved) { + throw new BadRequestException( + `حداکثر متراژ مجاز فاکتور برای «${row.name}» برابر ${maxMeterageResolved} متر است`, + ); + } + } + + let totalMeterage = 0; + const itemsData = dto.items.map((item) => { + const product = productMap.get(item.productId)!; + if (item.meterage < product.minMeterage) { + throw new BadRequestException( + `حداقل متراژ برای ${product.name} برابر ${product.minMeterage} است`, + ); + } + const rem = + Math.round((item.meterage / product.orderMultiplier) * 1000) / 1000; + if (Math.abs(rem - Math.round(rem)) > 1e-6) { + throw new BadRequestException( + `متراژ ${product.name} باید مضرب ${product.orderMultiplier} باشد`, + ); + } + const rollMeters = + product.rollMeters && product.rollMeters > 0 + ? product.rollMeters + : product.minMeterage > 0 + ? product.minMeterage + : 50; + if (item.meterage / rollMeters + 1e-9 < minRolls) { + throw new BadRequestException( + `حداقل سفارش برای ${product.name} برابر ${minRolls} طاقه است`, + ); + } + totalMeterage += item.meterage; + return { + productId: product.id, + catalogCode: product.catalog.code, + productName: product.name, + productCode: product.code, + color: product.color, + requestedMeterage: item.meterage, + notes: item.notes?.trim() || null, + companyId: product.companyId, + companyName: product.company.name, + }; + }); + + const companyNames = [ + ...new Set(itemsData.map((item) => item.companyName).filter(Boolean)), + ]; + // برای سازگاری با FK فعلی، اولین شرکت اقلام به‌عنوان شرکت اصلی سفارش نگه داشته می‌شود + const preferredCompanyId = itemsData.some( + (item) => item.companyId === dto.companyId, + ) + ? dto.companyId + : itemsData[0]!.companyId; + const primaryCompany = products.find((p) => p.companyId === preferredCompanyId) + ?.company; + if (!primaryCompany) { + throw new NotFoundException('شرکت محصولات یافت نشد یا غیرفعال است'); + } + + // روش تسویه و چک‌ها بعد از قیمت‌گذاری ادمین گرفته می‌شوند، نه اینجا. + const order = await this.createWithUniqueOrderNumber((orderNumber) => + this.prisma.$transaction(async (tx) => { + const created = await tx.order.create({ + data: { + orderNumber, + userId, + companyId: primaryCompany.id, + companyName: companyNames.join('، '), + settlementType: null, + checkMode: null, + status: OrderStatus.awaiting_review, + totalMeterage, + notes: dto.notes?.trim() || null, + items: { create: itemsData }, + statusHistory: { + create: [ + { + fromStatus: null, + toStatus: OrderStatus.awaiting_review, + changedBy: 'customer', + note: 'سفارش ثبت شد و برای بررسی مدیریت ارسال شد', + }, + ], + }, + }, + include: orderInclude, + }); + + // کل اقلام ثبت‌شده از سبد حذف می‌شوند (یک فاکتور واحد) + const cart = await tx.cart.findFirst({ where: { userId } }); + if (cart) { + await tx.cartItem.deleteMany({ + where: { + cartId: cart.id, + productId: { in: uniqueProductIds }, + }, + }); + const remaining = await tx.cartItem.findFirst({ + where: { cartId: cart.id }, + }); + await tx.cart.update({ + where: { id: cart.id }, + data: { companyId: remaining?.companyId ?? null }, + }); + } + + return created; + }), + ); + + this.notifications.dispatchOrderEvent(order.id, 'order_submitted'); + this.notifications.dispatchAdmin({ + title: 'سفارش جدید ثبت شد', + body: `سفارش ${order.orderNumber} با ${order.items.length} قلم و ${order.totalMeterage} متر در انتظار قیمت‌گذاری است.`, + link: `/orders/${order.id}`, + }); + + return this.mapOrder(order); + } + + /** + * مشتری بعد از دیدن جمع فاکتور روش تسویه را انتخاب می‌کند. + * نقدی مستقیم به مرحله پرداخت می‌رود، چکی به مرحله ثبت چک‌ها. + */ + async chooseSettlement(userId: string, id: string, dto: ChooseSettlementDto) { + const order = await this.getOrder(id); + if (order.userId !== userId) { + throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); + } + if (order.status !== OrderStatus.awaiting_settlement) { + throw new BadRequestException( + 'انتخاب روش تسویه فقط پس از صدور فاکتور توسط مدیریت امکان‌پذیر است', + ); + } + + const isCash = dto.settlementType === SettlementType.cash; + const nextStatus = isCash + ? OrderStatus.awaiting_cash_payment + : OrderStatus.check_in_progress; + assertTransition(order.status, nextStatus); + + const cashCompanies = isCash + ? await this.companiesForOrder(order) + : []; + + await this.prisma.$transaction(async (tx) => { + await tx.order.update({ + where: { id }, + data: { + settlementType: dto.settlementType, + checkMode: isCash + ? null + : (dto.checkMode ?? CheckRegistrationMode.full), + cashInvoiceExpiresAt: isCash + ? earliestCashInvoiceExpiry(cashCompanies) + : null, + cashReminderSentAt: null, + status: nextStatus, + }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: id, + fromStatus: order.status, + toStatus: nextStatus, + changedBy: 'customer', + note: isCash + ? 'مشتری تسویه نقدی را انتخاب کرد' + : 'مشتری تسویه چکی را انتخاب کرد', + }, + }); + }); + + return this.findMyOrder(userId, id); + } + + async adminCreate(adminId: string, dto: AdminCreateOrderDto) { + const company = await this.prisma.company.findUnique({ + where: { id: dto.companyId }, + }); + if (!company) throw new NotFoundException('شرکت یافت نشد'); + + let userId = dto.customerId || dto.userId; + if (userId) { + const existing = await this.prisma.customer.findUnique({ + where: { id: userId }, + }); + if (!existing) { + throw new BadRequestException('مشتری انتخاب‌شده یافت نشد'); + } + } else if (dto.customerPhone) { + const phone = normalizePhone(toEnglishDigits(dto.customerPhone)); + if (!isValidPhone(phone)) { + throw new BadRequestException('شماره موبایل نامعتبر است'); + } + let customer = await this.prisma.customer.findUnique({ + where: { phone }, + }); + if (!customer) { + const names = splitFullName(dto.customerName || 'مشتری جدید'); + customer = await this.prisma.customer.create({ + data: { + phone, + firstName: names.firstName, + lastName: names.lastName || 'جدید', + }, + }); + } + userId = customer.id; + } + if (!userId) { + throw new BadRequestException('انتخاب مشتری الزامی است'); + } + + const totalMeterage = dto.meterage ?? 0; + const ownerId = userId; + + const order = await this.createWithUniqueOrderNumber((orderNumber) => + this.prisma.order.create({ + data: { + orderNumber, + userId: ownerId, + companyId: company.id, + companyName: company.name, + settlementType: null, + status: OrderStatus.awaiting_review, + totalMeterage, + notes: dto.notes?.trim() || null, + statusHistory: { + create: { + fromStatus: null, + toStatus: OrderStatus.awaiting_review, + changedBy: `admin:${adminId}`, + note: 'سفارش توسط ادمین ثبت شد', + }, + }, + }, + include: orderInclude, + }), + ); + + return this.mapOrder(order, true); + } + + async updatePricing(id: string, adminId: string, dto: UpdateOrderPricingDto) { + const order = await this.getOrder(id); + + const unknown = dto.items.find( + (item) => !order.items.some((i) => i.id === item.id), + ); + if (unknown) { + throw new BadRequestException(`آیتم ${unknown.id} یافت نشد`); + } + + await this.prisma.$transaction(async (tx) => { + let totalAmount = 0; + let totalMeterage = 0; + + // جمع کل باید روی همه آیتم‌های سفارش حساب شود، نه فقط آیتم‌های ارسالی + for (const existing of order.items) { + const item = dto.items.find((i) => i.id === existing.id); + const unitPrice = + item?.unitPrice !== undefined + ? item.unitPrice + : item?.supplyPrice !== undefined + ? item.supplyPrice + : (existing.unitPrice ?? 0); + const unitDiscount = item?.unitDiscount ?? existing.unitDiscount ?? 0; + if (unitDiscount > unitPrice) { + throw new BadRequestException( + 'تخفیف واحد نمی‌تواند از قیمت واحد بیشتر باشد', + ); + } + const confirmed = + item?.confirmedMeterage ?? + existing.confirmedMeterage ?? + existing.requestedMeterage; + const lineTotal = roundMoney( + Math.max(0, (unitPrice - unitDiscount) * confirmed), + ); + totalAmount += lineTotal; + totalMeterage += confirmed; + + if (!item) continue; + + await tx.orderItem.update({ + where: { id: existing.id }, + data: { + confirmedMeterage: confirmed, + unitPrice, + unitDiscount, + lineTotal, + }, + }); + } + totalAmount = roundMoney(totalAmount); + + const shippingCost = dto.shippingCost ?? order.shippingCost ?? 0; + const totalDiscount = dto.totalDiscount ?? order.totalDiscount ?? 0; + const finalAmount = roundMoney( + Math.max(0, totalAmount + shippingCost - totalDiscount), + ); + + const nextStatus = + order.status === OrderStatus.awaiting_review || + order.status === OrderStatus.pricing + ? OrderStatus.pricing + : order.status; + + await tx.order.update({ + where: { id }, + data: { + totalAmount, + totalMeterage, + shippingCost, + totalDiscount, + finalAmount, + status: nextStatus, + }, + }); + + if (nextStatus !== order.status) { + await tx.orderStatusHistory.create({ + data: { + orderId: id, + fromStatus: order.status, + toStatus: nextStatus, + changedBy: `admin:${adminId}`, + note: dto.note || 'قیمت‌گذاری انجام شد', + }, + }); + } + }); + + return this.findAdminOrder(id); + } + + async updateStatus(id: string, adminId: string, dto: UpdateOrderStatusDto) { + const order = await this.getOrder(id); + if (order.status === dto.status) { + return this.mapOrder(order, true); + } + + if (dto.force) { + if (!dto.reason?.trim()) { + throw new BadRequestException( + 'برای تغییر دستی وضعیت، ذکر دلیل الزامی است', + ); + } + } else { + assertTransition(order.status, dto.status); + } + + // فاکتور بدون مبلغ نباید برای مشتری ارسال شود + if ( + dto.status === OrderStatus.awaiting_settlement && + (order.finalAmount ?? 0) <= 0 + ) { + throw new BadRequestException( + 'ابتدا سفارش را قیمت‌گذاری کنید تا فاکتور قابل ارسال باشد', + ); + } + + const needsCashExpiry = + (dto.status === OrderStatus.cash_invoice_issued || + dto.status === OrderStatus.awaiting_cash_payment) && + !order.cashInvoiceExpiresAt; + const cashExpiry = needsCashExpiry + ? earliestCashInvoiceExpiry(await this.companiesForOrder(order)) + : null; + + await this.prisma.$transaction(async (tx) => { + await tx.order.update({ + where: { id }, + data: { status: dto.status }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: id, + fromStatus: order.status, + toStatus: dto.status, + changedBy: `admin:${adminId}`, + note: dto.force + ? `تغییر دستی وضعیت${dto.note ? ` — ${dto.note}` : ''}` + : dto.note || null, + reason: dto.reason || null, + }, + }); + + if (cashExpiry) { + await tx.order.update({ + where: { id }, + data: { cashInvoiceExpiresAt: cashExpiry }, + }); + } + }); + + const event = eventForStatus(dto.status); + if (event) { + this.notifications.dispatchOrderEvent(id, event, { + note: dto.reason ?? dto.note ?? null, + }); + } + + return this.findAdminOrder(id); + } + + async updateChecks(userId: string, id: string, dto: UpdateOrderChecksDto) { + const order = await this.getOrder(id); + if (order.userId !== userId) { + throw new ForbiddenException('دسترسی به این سفارش مجاز نیست'); + } + if (order.settlementType !== SettlementType.check) { + throw new BadRequestException('این سفارش تسویه چکی نیست'); + } + if (!CHECK_EDITABLE_STATUSES.includes(order.status)) { + throw new BadRequestException( + 'در وضعیت فعلی سفارش امکان ویرایش چک‌ها وجود ندارد', + ); + } + + if (!dto.checks.length) { + throw new BadRequestException('حداقل یک چک لازم است'); + } + + const checkMode = + dto.checkMode ?? order.checkMode ?? CheckRegistrationMode.full; + const rass = calculateCheckRass( + dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), + order.createdAt, + ); + + // ثبت اولیه و اصلاح، هر دو سفارش را به صف بررسی چک می‌برند. + const goesToReview = + order.status === OrderStatus.check_in_progress || + order.status === OrderStatus.check_needs_fix || + order.status === OrderStatus.check_rejected; + const nextStatus = goesToReview + ? OrderStatus.awaiting_check_review + : order.status; + const isFix = order.status !== OrderStatus.check_in_progress; + + await this.prisma.$transaction(async (tx) => { + await tx.check.deleteMany({ where: { orderId: id } }); + await tx.check.createMany({ + data: dto.checks.map((c, index) => ({ + orderId: id, + ...this.mapCheckCreate(c, index + 1, checkMode), + })), + }); + await tx.order.update({ + where: { id }, + data: { + checkMode, + checkRassDays: rass.rassDays, + checkRassDate: rass.rassDate ? new Date(rass.rassDate) : null, + status: nextStatus, + }, + }); + if (nextStatus !== order.status) { + await tx.orderStatusHistory.create({ + data: { + orderId: id, + fromStatus: order.status, + toStatus: nextStatus, + changedBy: 'customer', + note: isFix + ? 'چک‌ها اصلاح و مجدداً ارسال شد' + : 'چک‌ها برای بررسی ارسال شد', + }, + }); + } + }); + + if (nextStatus !== order.status) { + this.notifications.dispatchAdmin({ + title: isFix ? 'چک‌های اصلاح‌شده رسید' : 'چک‌های سفارش ثبت شد', + body: `مشتری ${dto.checks.length} چک برای سفارش ${order.orderNumber} ثبت کرد و منتظر بررسی است.`, + link: `/orders/${id}/checks`, + }); + } + + return this.findMyOrder(userId, id); + } + + private mapCheckCreate( + c: SubmitOrderCheckDto, + row: number, + defaultMode: CheckRegistrationMode, + ) { + return { + row, + amount: c.amount, + dueDate: new Date(c.dueDate), + sayadId: c.sayadId?.trim() || null, + checkNumber: c.checkNumber?.trim() || null, + ownerNationalId: c.ownerNationalId?.trim() || null, + ownerFirstName: c.ownerFirstName?.trim() || null, + ownerLastName: c.ownerLastName?.trim() || null, + bankName: c.bankName?.trim() || null, + branchName: c.branchName?.trim() || null, + frontImage: c.frontImage || null, + backImage: c.backImage || null, + notes: c.notes?.trim() || null, + mode: c.mode ?? defaultMode, + }; + } + + private async nextOrderNumber(): Promise { + for (let i = 0; i < 5; i++) { + const orderNumber = generateOrderNumber(); + const exists = await this.prisma.order.findUnique({ + where: { orderNumber }, + }); + if (!exists) return orderNumber; + } + return `${generateOrderNumber()}-${Date.now().toString().slice(-3)}`; + } + + /** بین انتخاب شماره و درج، امکان تصاحب شماره توسط درخواست هم‌زمان هست. */ + private async createWithUniqueOrderNumber( + create: (orderNumber: string) => Promise, + ): Promise { + for (let attempt = 0; ; attempt++) { + const orderNumber = await this.nextOrderNumber(); + try { + return await create(orderNumber); + } catch (e) { + const isDuplicate = + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002'; + if (!isDuplicate || attempt >= 4) throw e; + } + } + } + + private async companiesForOrder(order: OrderFull) { + const companyIds = [ + ...new Set( + [ + order.companyId, + ...order.items.map((item) => item.companyId).filter(Boolean), + ].filter(Boolean), + ), + ]; + if (!companyIds.length) return order.company ? [order.company] : []; + const companies = await this.prisma.company.findMany({ + where: { id: { in: companyIds } }, + }); + return companies.length ? companies : order.company ? [order.company] : []; + } + + private async getOrder(id: string): Promise { + const order = await this.prisma.order.findUnique({ + where: { id }, + include: orderInclude, + }); + if (!order) throw new NotFoundException('سفارش یافت نشد'); + return order; + } + + mapOrder(order: OrderFull, adminView = false) { + const customerName = joinFullName( + order.user.firstName, + order.user.lastName, + ); + const hidePrices = + !adminView && PRICE_HIDDEN_STATUSES.includes(order.status); + const base = { + id: order.id, + orderNumber: order.orderNumber, + userId: order.userId, + companyId: order.companyId, + companyName: order.companyName, + settlementType: order.settlementType, + settlementMethod: order.settlementType, + checkMode: order.checkMode ?? undefined, + status: order.status, + adminStatus: toAdminStatus(order.status), + allowedTransitions: adminView ? allowedTransitions(order.status) : [], + totalMeterage: order.totalMeterage, + meterage: order.totalMeterage, + totalAmount: hidePrices ? null : order.totalAmount, + shippingCost: hidePrices ? null : order.shippingCost, + totalDiscount: hidePrices ? null : order.totalDiscount, + finalAmount: hidePrices ? null : order.finalAmount, + pricesVisible: !hidePrices, + notes: order.notes ?? undefined, + checkRassDays: order.checkRassDays ?? undefined, + checkRassDate: order.checkRassDate?.toISOString(), + cashInvoiceExpiresAt: order.cashInvoiceExpiresAt?.toISOString(), + createdAt: order.createdAt.toISOString(), + updatedAt: order.updatedAt.toISOString(), + customerName, + customerPhone: order.user.phone, + customerNationalId: order.user.nationalId ?? undefined, + customerType: order.user.customerType, + items: order.items.map((item) => ({ + id: item.id, + productId: item.productId ?? undefined, + catalogCode: item.catalogCode, + productName: item.productName, + productCode: item.productCode ?? undefined, + color: item.color, + requestedMeterage: item.requestedMeterage, + confirmedMeterage: item.confirmedMeterage, + unitPrice: hidePrices ? null : item.unitPrice, + supplyPrice: hidePrices ? null : item.unitPrice, + unitDiscount: hidePrices ? null : (item.unitDiscount ?? 0), + discount: hidePrices ? null : (item.unitDiscount ?? 0), + lineTotal: hidePrices ? null : item.lineTotal, + totalAmount: hidePrices ? null : item.lineTotal, + notes: item.notes ?? undefined, + companyId: item.companyId, + companyName: item.companyName, + })), + checks: order.checks.map((c) => ({ + id: c.id, + orderId: c.orderId, + row: c.row, + amount: c.amount, + dueDate: c.dueDate.toISOString(), + sayadId: c.sayadId ?? undefined, + checkNumber: c.checkNumber ?? undefined, + ownerNationalId: c.ownerNationalId ?? undefined, + ownerFirstName: c.ownerFirstName ?? undefined, + ownerLastName: c.ownerLastName ?? undefined, + bankName: c.bankName ?? '', + branchName: c.branchName ?? undefined, + frontImage: c.frontImage ?? undefined, + backImage: c.backImage ?? undefined, + notes: c.notes ?? undefined, + mode: c.mode, + status: c.status, + createdAt: c.createdAt.toISOString(), + updatedAt: c.updatedAt.toISOString(), + })), + cashReceipt: order.cashReceipt + ? { + id: order.cashReceipt.id, + amount: order.cashReceipt.amount, + paidAmount: order.cashReceipt.paidAmount, + transferDate: order.cashReceipt.transferDate?.toISOString() ?? null, + transferTime: order.cashReceipt.transferTime ?? undefined, + trackingNumber: order.cashReceipt.trackingNumber ?? undefined, + trackingCode: order.cashReceipt.trackingNumber ?? undefined, + destinationAccountId: + order.cashReceipt.destinationAccountId ?? undefined, + receiptImage: order.cashReceipt.receiptImage ?? undefined, + notes: order.cashReceipt.notes ?? undefined, + expiresAt: + order.cashReceipt.expiresAt?.toISOString() ?? + order.cashInvoiceExpiresAt?.toISOString(), + status: order.cashReceipt.status, + submittedAt: order.cashReceipt.submittedAt?.toISOString(), + createdAt: order.cashReceipt.createdAt.toISOString(), + } + : undefined, + shipment: order.shipment + ? { + id: order.shipment.id, + orderId: order.shipment.orderId, + carrier: order.shipment.carrier, + trackingNumber: order.shipment.trackingNumber ?? '', + meterage: order.shipment.meterage, + status: order.shipment.status, + shippedAt: order.shipment.shippedAt?.toISOString() ?? null, + deliveredAt: order.shipment.deliveredAt?.toISOString() ?? null, + } + : undefined, + statusHistory: order.statusHistory.map((h) => ({ + id: h.id, + fromStatus: h.fromStatus, + toStatus: h.toStatus, + changedBy: h.changedBy, + note: h.note ?? undefined, + reason: h.reason ?? undefined, + changedAt: h.changedAt.toISOString(), + })), + }; + + return base; + } +} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..7ffd32d --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,16 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/products/dto/product.dto.ts b/src/products/dto/product.dto.ts new file mode 100644 index 0000000..e53ad8b --- /dev/null +++ b/src/products/dto/product.dto.ts @@ -0,0 +1,268 @@ +import { + IsBoolean, + IsNumber, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateProductDto { + @IsString() + companyId!: string; + + @IsString() + catalogId!: string; + + @IsString() + @MinLength(1) + name!: string; + + @IsString() + @MinLength(1) + code!: string; + + /** Alias accepted from admin UI */ + @IsOptional() + @IsString() + productCode?: string; + + @IsString() + @MinLength(1) + color!: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + minMeterage?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0.001) + orderMultiplier?: number; + + /** Alias from admin UI */ + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0.001) + orderStep?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + basePrice?: number | null; + + /** Alias from admin UI */ + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + price?: number | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + stock?: number | null; + + /** Alias from admin UI */ + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + internalStock?: number | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + usageCategory?: string; + + @IsOptional() + @IsString() + season?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + widthCm?: number | null; + + @IsOptional() + @IsString() + composition?: string | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + weightGrams?: number | null; + + @IsOptional() + @IsString() + weaveType?: string | null; + + @IsOptional() + @IsString() + subtitle?: string | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + rollMeters?: number; + + @IsOptional() + @IsBoolean() + isNew?: boolean; + + @IsOptional() + colorsJson?: string[]; + + @IsOptional() + imageUrlsJson?: string[]; + + @IsOptional() + tagsJson?: string[]; +} + +export class UpdateProductDto { + @IsOptional() + @IsString() + catalogId?: string; + + @IsOptional() + @IsString() + @MinLength(1) + name?: string; + + @IsOptional() + @IsString() + @MinLength(1) + code?: string; + + @IsOptional() + @IsString() + productCode?: string; + + @IsOptional() + @IsString() + color?: string; + + @IsOptional() + @IsString() + description?: string | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + minMeterage?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0.001) + orderMultiplier?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0.001) + orderStep?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + basePrice?: number | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + price?: number | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + stock?: number | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + internalStock?: number | null; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + usageCategory?: string; + + @IsOptional() + @IsString() + season?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + widthCm?: number | null; + + @IsOptional() + @IsString() + composition?: string | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + weightGrams?: number | null; + + @IsOptional() + @IsString() + weaveType?: string | null; + + @IsOptional() + @IsString() + subtitle?: string | null; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + rollMeters?: number; + + @IsOptional() + @IsBoolean() + isNew?: boolean; + + @IsOptional() + colorsJson?: string[]; + + @IsOptional() + imageUrlsJson?: string[]; + + @IsOptional() + tagsJson?: string[]; +} + +export class SetProductActiveDto { + @IsBoolean() + isActive!: boolean; +} diff --git a/src/products/products.controller.ts b/src/products/products.controller.ts new file mode 100644 index 0000000..61bd232 --- /dev/null +++ b/src/products/products.controller.ts @@ -0,0 +1,105 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { ProductsService } from './products.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + CreateProductDto, + SetProductActiveDto, + UpdateProductDto, +} from './dto/product.dto'; + +@Controller('products') +export class ProductsController { + constructor(private readonly productsService: ProductsService) {} + + @AuthType('any') + @Get('meta/categories') + categories() { + return this.productsService.listCatalogCategories(); + } + + @AuthType('any') + @Get() + findAll( + @CurrentUser() user: AuthPrincipal | undefined, + @Query('catalogId') catalogId?: string, + @Query('companyId') companyId?: string, + @Query('color') color?: string, + @Query('activeOnly') activeOnly?: string, + @Query('categoryId') categoryId?: string, + @Query('usageCategory') usageCategory?: string, + @Query('season') season?: string, + ) { + const isAdmin = user?.type === 'admin'; + return this.productsService.findAll({ + catalogId, + companyId, + color, + activeOnly: activeOnly === 'true' || activeOnly === '1', + usageCategory: usageCategory || categoryId, + season, + hidePublicPrice: !isAdmin, + }); + } + + @AuthType('customer') + @Get('favorites/ids') + favoriteIds(@CurrentUser() user: AuthPrincipal) { + return this.productsService.listFavoriteIds(user.id); + } + + @AuthType('customer') + @Post(':id/favorite') + toggleFavorite( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + ) { + return this.productsService.toggleFavorite(user.id, id); + } + + @AuthType('customer') + @Delete(':id/favorite') + removeFavorite( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + ) { + return this.productsService.removeFavorite(user.id, id); + } + + @AuthType('any') + @Get(':id') + findOne( + @CurrentUser() user: AuthPrincipal | undefined, + @Param('id') id: string, + ) { + return this.productsService.findOne(id, user?.type !== 'admin'); + } + + @AuthType('admin') + @Post() + create(@Body() dto: CreateProductDto) { + return this.productsService.create(dto); + } + + @AuthType('admin') + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateProductDto) { + return this.productsService.update(id, dto); + } + + @AuthType('admin') + @Patch(':id/active') + setActive(@Param('id') id: string, @Body() dto: SetProductActiveDto) { + return this.productsService.setActive(id, dto.isActive); + } +} diff --git a/src/products/products.module.ts b/src/products/products.module.ts new file mode 100644 index 0000000..883c79f --- /dev/null +++ b/src/products/products.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ProductsController } from './products.controller'; +import { ProductsService } from './products.service'; + +@Module({ + controllers: [ProductsController], + providers: [ProductsService], + exports: [ProductsService], +}) +export class ProductsModule {} diff --git a/src/products/products.service.ts b/src/products/products.service.ts new file mode 100644 index 0000000..37d256e --- /dev/null +++ b/src/products/products.service.ts @@ -0,0 +1,354 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateProductDto, UpdateProductDto } from './dto/product.dto'; +import { loadCatalogCategories } from '../catalog/category-defs'; + +@Injectable() +export class ProductsService { + constructor(private readonly prisma: PrismaService) {} + + listCatalogCategories() { + return loadCatalogCategories(this.prisma); + } + + async findAll(filters: { + catalogId?: string; + companyId?: string; + color?: string; + activeOnly?: boolean; + usageCategory?: string; + season?: string; + hidePublicPrice?: boolean; + }) { + const products = await this.prisma.product.findMany({ + where: { + ...(filters.catalogId ? { catalogId: filters.catalogId } : {}), + ...(filters.companyId ? { companyId: filters.companyId } : {}), + ...(filters.color + ? { color: { contains: filters.color, mode: 'insensitive' } } + : {}), + ...(filters.activeOnly ? { isActive: true } : {}), + ...(filters.usageCategory + ? { usageCategory: filters.usageCategory as never } + : {}), + ...(filters.season ? { season: filters.season as never } : {}), + }, + orderBy: [{ isNew: 'desc' }, { name: 'asc' }, { color: 'asc' }], + include: { + company: { select: { name: true } }, + catalog: { select: { code: true, name: true } }, + }, + }); + const hidePrice = await this.shouldHidePublicPrice(filters.hidePublicPrice); + return products.map((p) => this.map(p, hidePrice)); + } + + async findOne(id: string, hidePublicPrice?: boolean) { + const product = await this.prisma.product.findUnique({ + where: { id }, + include: { + company: { select: { name: true } }, + catalog: { select: { code: true, name: true } }, + }, + }); + if (!product) throw new NotFoundException('محصول یافت نشد'); + const hidePrice = await this.shouldHidePublicPrice(hidePublicPrice); + return this.map(product, hidePrice); + } + + private async shouldHidePublicPrice(forceHide?: boolean) { + if (forceHide === false) return false; + if (forceHide === true) { + const settings = await this.prisma.systemSettings.findUnique({ + where: { id: 'default' }, + }); + return !(settings?.showCatalogPublicPrice ?? false); + } + return false; + } + + async create(dto: CreateProductDto) { + const catalog = await this.prisma.catalog.findUnique({ + where: { id: dto.catalogId }, + }); + if (!catalog) throw new NotFoundException('کاتالوگ یافت نشد'); + if (catalog.companyId !== dto.companyId) { + throw new ConflictException('کاتالوگ متعلق به این شرکت نیست'); + } + + const code = (dto.code || dto.productCode || '').trim(); + if (!code) throw new ConflictException('کد محصول الزامی است'); + + try { + const product = await this.prisma.product.create({ + data: { + companyId: dto.companyId, + catalogId: dto.catalogId, + name: dto.name.trim(), + code, + color: dto.color.trim(), + description: dto.description?.trim() || null, + minMeterage: dto.minMeterage ?? 30, + orderMultiplier: dto.orderMultiplier ?? dto.orderStep ?? 10, + basePrice: this.pickNumber(dto.basePrice, dto.price), + stock: this.pickNumber(dto.stock, dto.internalStock), + isActive: dto.isActive ?? true, + usageCategory: (dto.usageCategory as never) ?? undefined, + season: (dto.season as never) ?? undefined, + widthCm: dto.widthCm, + composition: dto.composition?.trim() || null, + weightGrams: dto.weightGrams, + weaveType: dto.weaveType?.trim() || null, + subtitle: dto.subtitle?.trim() || null, + rollMeters: dto.rollMeters, + isNew: dto.isNew, + colorsJson: dto.colorsJson ?? undefined, + imageUrlsJson: dto.imageUrlsJson ?? undefined, + tagsJson: dto.tagsJson ?? undefined, + }, + include: { + company: { select: { name: true } }, + catalog: { select: { code: true, name: true } }, + }, + }); + return this.map(product); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('کد محصول در این شرکت تکراری است'); + } + throw e; + } + } + + async update(id: string, dto: UpdateProductDto) { + await this.ensure(id); + const code = + dto.code !== undefined + ? dto.code.trim() + : dto.productCode !== undefined + ? dto.productCode.trim() + : undefined; + + try { + const product = await this.prisma.product.update({ + where: { id }, + data: { + catalogId: dto.catalogId, + name: dto.name?.trim(), + code, + color: dto.color?.trim(), + description: + dto.description === undefined + ? undefined + : dto.description?.trim() || null, + minMeterage: dto.minMeterage, + orderMultiplier: + dto.orderMultiplier !== undefined + ? dto.orderMultiplier + : dto.orderStep, + basePrice: + dto.basePrice !== undefined + ? dto.basePrice + : dto.price !== undefined + ? dto.price + : undefined, + stock: + dto.stock !== undefined + ? dto.stock + : dto.internalStock !== undefined + ? dto.internalStock + : undefined, + isActive: dto.isActive, + usageCategory: + dto.usageCategory !== undefined + ? (dto.usageCategory as never) + : undefined, + season: dto.season !== undefined ? (dto.season as never) : undefined, + widthCm: dto.widthCm, + composition: + dto.composition === undefined + ? undefined + : dto.composition?.trim() || null, + weightGrams: dto.weightGrams, + weaveType: + dto.weaveType === undefined + ? undefined + : dto.weaveType?.trim() || null, + subtitle: + dto.subtitle === undefined + ? undefined + : dto.subtitle?.trim() || null, + rollMeters: dto.rollMeters, + isNew: dto.isNew, + colorsJson: dto.colorsJson, + imageUrlsJson: dto.imageUrlsJson, + tagsJson: dto.tagsJson, + }, + include: { + company: { select: { name: true } }, + catalog: { select: { code: true, name: true } }, + }, + }); + return this.map(product); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === 'P2002' + ) { + throw new ConflictException('کد محصول در این شرکت تکراری است'); + } + throw e; + } + } + + async setActive(id: string, isActive: boolean) { + await this.ensure(id); + const product = await this.prisma.product.update({ + where: { id }, + data: { isActive }, + include: { + company: { select: { name: true } }, + catalog: { select: { code: true, name: true } }, + }, + }); + return this.map(product); + } + + async listFavoriteIds(customerId: string) { + const rows = await this.prisma.productFavorite.findMany({ + where: { customerId }, + select: { productId: true }, + orderBy: { createdAt: 'desc' }, + }); + return rows.map((r) => r.productId); + } + + async toggleFavorite(customerId: string, productId: string) { + await this.ensure(productId); + const existing = await this.prisma.productFavorite.findUnique({ + where: { + customerId_productId: { customerId, productId }, + }, + }); + if (existing) { + await this.prisma.productFavorite.delete({ where: { id: existing.id } }); + return { productId, favorited: false }; + } + await this.prisma.productFavorite.create({ + data: { customerId, productId }, + }); + return { productId, favorited: true }; + } + + async removeFavorite(customerId: string, productId: string) { + await this.prisma.productFavorite.deleteMany({ + where: { customerId, productId }, + }); + return { productId, favorited: false }; + } + + private pickNumber( + primary?: number | null, + alias?: number | null, + ): number | null { + if (primary !== undefined) return primary; + if (alias !== undefined) return alias; + return null; + } + + private async ensure(id: string) { + const product = await this.prisma.product.findUnique({ where: { id } }); + if (!product) throw new NotFoundException('محصول یافت نشد'); + return product; + } + + private map( + product: { + id: string; + companyId: string; + catalogId: string; + name: string; + code: string; + color: string; + description: string | null; + minMeterage: number; + orderMultiplier: number; + basePrice: number | null; + stock: number | null; + isActive: boolean; + createdAt: Date; + updatedAt: Date; + company: { name: string }; + catalog: { code: string; name: string }; + usageCategory?: string; + season?: string; + widthCm?: number | null; + composition?: string | null; + weightGrams?: number | null; + weaveType?: string | null; + colorsJson?: unknown; + tagsJson?: unknown; + imageUrlsJson?: unknown; + subtitle?: string | null; + rollMeters?: number; + isNew?: boolean; + }, + hidePrice = false, + ) { + const colors = Array.isArray(product.colorsJson) + ? (product.colorsJson as string[]) + : product.color + ? [product.color] + : []; + const tags = Array.isArray(product.tagsJson) + ? (product.tagsJson as string[]) + : []; + const price = hidePrice ? null : product.basePrice; + return { + id: product.id, + companyId: product.companyId, + companyName: product.company.name, + catalogId: product.catalogId, + catalogCode: product.catalog.code, + catalogName: product.catalog.name, + name: product.name, + code: product.code, + productCode: product.code, + color: product.color, + description: product.description ?? undefined, + minMeterage: product.minMeterage, + orderMultiplier: product.orderMultiplier, + orderStep: product.orderMultiplier, + basePrice: price, + price, + stock: product.stock, + internalStock: product.stock ?? 0, + isActive: product.isActive, + categoryId: product.usageCategory ?? 'other', + usageCategory: product.usageCategory ?? 'other', + season: product.season ?? 'all_season', + widthCm: product.widthCm ?? undefined, + composition: product.composition ?? undefined, + weightGrams: product.weightGrams ?? undefined, + weaveType: product.weaveType ?? undefined, + colors, + tags, + imageUrls: Array.isArray(product.imageUrlsJson) + ? product.imageUrlsJson + : [], + subtitle: product.subtitle ?? undefined, + rollMeters: product.rollMeters ?? 50, + isNew: product.isNew ?? false, + createdAt: product.createdAt.toISOString(), + updatedAt: product.updatedAt.toISOString(), + }; + } +} diff --git a/src/settings/dto/settings.dto.ts b/src/settings/dto/settings.dto.ts new file mode 100644 index 0000000..76d5c1c --- /dev/null +++ b/src/settings/dto/settings.dto.ts @@ -0,0 +1,76 @@ +import { IsBoolean, IsInt, IsObject, IsOptional, IsString, Min } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class UpdateSettingsDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + maxMeteragePerInvoice?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + cashInvoiceValidityHours?: number; + + @IsOptional() + @IsBoolean() + defaultSettlementCheck?: boolean; + + @IsOptional() + @IsBoolean() + defaultSettlementCash?: boolean; + + @IsOptional() + @IsBoolean() + smsOrderRegistered?: boolean; + + @IsOptional() + @IsBoolean() + smsPricingReady?: boolean; + + @IsOptional() + @IsBoolean() + smsPaymentReminder?: boolean; + + @IsOptional() + @IsBoolean() + smsCheckReview?: boolean; + + @IsOptional() + @IsBoolean() + smsShippingUpdate?: boolean; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + minOrderRollsPerProduct?: number; + + @IsOptional() + @IsBoolean() + showCatalogPublicPrice?: boolean; + + @IsOptional() + @IsBoolean() + allowAgentPricing?: boolean; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + agentMinUnitPrice?: number | null; + + @IsOptional() + @IsString() + supportPhone?: string | null; + + @IsOptional() + @IsString() + supportEmail?: string | null; + + @IsOptional() + @IsObject() + categoryImagesJson?: Record; +} diff --git a/src/settings/settings.controller.ts b/src/settings/settings.controller.ts new file mode 100644 index 0000000..1ebea63 --- /dev/null +++ b/src/settings/settings.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, Get, Patch } from '@nestjs/common'; +import { SettingsService } from './settings.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { UpdateSettingsDto } from './dto/settings.dto'; + +@Controller('admin/settings') +@AuthType('admin') +@Roles('admin') +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + @Get() + get() { + return this.settingsService.get(); + } + + @Patch() + update(@Body() dto: UpdateSettingsDto) { + return this.settingsService.update(dto); + } +} diff --git a/src/settings/settings.module.ts b/src/settings/settings.module.ts new file mode 100644 index 0000000..f25bf63 --- /dev/null +++ b/src/settings/settings.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SettingsController } from './settings.controller'; +import { SettingsService } from './settings.service'; + +@Module({ + controllers: [SettingsController], + providers: [SettingsService], + exports: [SettingsService], +}) +export class SettingsModule {} diff --git a/src/settings/settings.service.ts b/src/settings/settings.service.ts new file mode 100644 index 0000000..ad915b1 --- /dev/null +++ b/src/settings/settings.service.ts @@ -0,0 +1,126 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { UpdateSettingsDto } from './dto/settings.dto'; +import { CATEGORY_IMAGE_KEYS, asImageMap } from '../catalog/category-defs'; + +@Injectable() +export class SettingsService { + constructor(private readonly prisma: PrismaService) {} + + async get() { + const settings = await this.ensureDefaults(); + return this.map(settings); + } + + async update(dto: UpdateSettingsDto) { + await this.ensureDefaults(); + const settings = await this.prisma.systemSettings.update({ + where: { id: 'default' }, + data: { + maxMeteragePerInvoice: dto.maxMeteragePerInvoice, + cashInvoiceValidityHours: dto.cashInvoiceValidityHours, + defaultSettlementCheck: dto.defaultSettlementCheck, + defaultSettlementCash: dto.defaultSettlementCash, + minOrderRollsPerProduct: dto.minOrderRollsPerProduct, + showCatalogPublicPrice: dto.showCatalogPublicPrice, + allowAgentPricing: dto.allowAgentPricing, + agentMinUnitPrice: + dto.agentMinUnitPrice === undefined + ? undefined + : dto.agentMinUnitPrice, + smsOrderRegistered: dto.smsOrderRegistered, + smsPricingReady: dto.smsPricingReady, + smsPaymentReminder: dto.smsPaymentReminder, + smsCheckReview: dto.smsCheckReview, + smsShippingUpdate: dto.smsShippingUpdate, + supportPhone: + dto.supportPhone === undefined + ? undefined + : dto.supportPhone?.trim() || null, + supportEmail: + dto.supportEmail === undefined + ? undefined + : dto.supportEmail?.trim() || null, + categoryImagesJson: + dto.categoryImagesJson === undefined + ? undefined + : this.normalizeCategoryImages(dto.categoryImagesJson), + }, + }); + return this.map(settings); + } + + private async ensureDefaults() { + return this.prisma.systemSettings.upsert({ + where: { id: 'default' }, + create: { id: 'default' }, + update: {}, + }); + } + + private map(settings: { + id: string; + maxMeteragePerInvoice: number; + cashInvoiceValidityHours: number; + defaultSettlementCheck: boolean; + defaultSettlementCash: boolean; + minOrderRollsPerProduct?: number; + showCatalogPublicPrice?: boolean; + allowAgentPricing?: boolean; + agentMinUnitPrice?: number | null; + smsOrderRegistered: boolean; + smsPricingReady: boolean; + smsPaymentReminder: boolean; + smsCheckReview: boolean; + smsShippingUpdate: boolean; + supportPhone: string | null; + supportEmail: string | null; + categoryImagesJson?: unknown; + updatedAt: Date; + }) { + return { + id: settings.id, + maxMeteragePerInvoice: settings.maxMeteragePerInvoice, + cashInvoiceValidityHours: settings.cashInvoiceValidityHours, + defaultSettlementCheck: settings.defaultSettlementCheck, + defaultSettlementCash: settings.defaultSettlementCash, + minOrderRollsPerProduct: settings.minOrderRollsPerProduct ?? 1, + showCatalogPublicPrice: settings.showCatalogPublicPrice ?? false, + allowAgentPricing: settings.allowAgentPricing ?? false, + agentMinUnitPrice: settings.agentMinUnitPrice ?? null, + smsOrderRegistered: settings.smsOrderRegistered, + smsPricingReady: settings.smsPricingReady, + smsPaymentReminder: settings.smsPaymentReminder, + smsCheckReview: settings.smsCheckReview, + smsShippingUpdate: settings.smsShippingUpdate, + supportPhone: settings.supportPhone, + supportEmail: settings.supportEmail, + categoryImagesJson: asImageMap(settings.categoryImagesJson), + updatedAt: settings.updatedAt.toISOString(), + }; + } + + private normalizeCategoryImages( + input: Record, + ): Record { + const allowed = new Set(CATEGORY_IMAGE_KEYS); + const next: Record = {}; + for (const [key, value] of Object.entries(input)) { + if (!allowed.has(key)) continue; + if (value == null || value === '') continue; + const image = value.trim(); + if ( + !image.startsWith('data:image/') && + !image.startsWith('https://') && + !image.startsWith('http://') + ) { + throw new BadRequestException('فرمت تصویر دسته نامعتبر است'); + } + if (image.startsWith('data:image/') && image.length > 1_600_000) { + throw new BadRequestException('حجم تصویر دسته خیلی زیاد است'); + } + next[key] = image; + } + return next; + } +} diff --git a/src/shipments/dto/shipment.dto.ts b/src/shipments/dto/shipment.dto.ts new file mode 100644 index 0000000..77540a3 --- /dev/null +++ b/src/shipments/dto/shipment.dto.ts @@ -0,0 +1,63 @@ +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + Min, + MinLength, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ShipmentStatus } from '@prisma/client'; + +export class CreateShipmentDto { + @IsString() + orderId!: string; + + @IsString() + @MinLength(1) + carrier!: string; + + @IsOptional() + @IsString() + trackingNumber?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + meterage?: number; + + @IsOptional() + @IsEnum(ShipmentStatus) + status?: ShipmentStatus; +} + +export class UpdateShipmentDto { + @IsOptional() + @IsString() + carrier?: string; + + @IsOptional() + @IsString() + trackingNumber?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + meterage?: number; + + @IsOptional() + @IsEnum(ShipmentStatus) + status?: ShipmentStatus; +} + +export class AdvanceShipmentStatusDto { + @IsOptional() + @IsEnum(ShipmentStatus) + status?: ShipmentStatus; + + @IsOptional() + @IsString() + trackingNumber?: string; +} diff --git a/src/shipments/shipments.controller.ts b/src/shipments/shipments.controller.ts new file mode 100644 index 0000000..cbb7250 --- /dev/null +++ b/src/shipments/shipments.controller.ts @@ -0,0 +1,55 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { ShipmentStatus } from '@prisma/client'; +import { ShipmentsService } from './shipments.service'; +import { AuthType, Roles } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; +import { + AdvanceShipmentStatusDto, + CreateShipmentDto, + UpdateShipmentDto, +} from './dto/shipment.dto'; + +@Controller('admin/shipments') +@AuthType('admin') +@Roles('admin', 'warehouse') +export class ShipmentsController { + constructor(private readonly shipmentsService: ShipmentsService) {} + + @Get() + findAll(@Query('status') status?: ShipmentStatus) { + return this.shipmentsService.findAll(status); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.shipmentsService.findOne(id); + } + + @Post() + create(@CurrentUser() user: AuthPrincipal, @Body() dto: CreateShipmentDto) { + return this.shipmentsService.create(user.id, dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateShipmentDto) { + return this.shipmentsService.update(id, dto); + } + + @Patch(':id/status') + advanceStatus( + @CurrentUser() user: AuthPrincipal, + @Param('id') id: string, + @Body() dto: AdvanceShipmentStatusDto, + ) { + return this.shipmentsService.advanceStatus(id, user.id, dto); + } +} diff --git a/src/shipments/shipments.module.ts b/src/shipments/shipments.module.ts new file mode 100644 index 0000000..2216330 --- /dev/null +++ b/src/shipments/shipments.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ShipmentsController } from './shipments.controller'; +import { ShipmentsService } from './shipments.service'; +import { NotificationsModule } from '../notifications/notifications.module'; + +@Module({ + imports: [NotificationsModule], + controllers: [ShipmentsController], + providers: [ShipmentsService], + exports: [ShipmentsService], +}) +export class ShipmentsModule {} diff --git a/src/shipments/shipments.service.ts b/src/shipments/shipments.service.ts new file mode 100644 index 0000000..cf654d6 --- /dev/null +++ b/src/shipments/shipments.service.ts @@ -0,0 +1,247 @@ +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { OrderStatus, ShipmentStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { + AdvanceShipmentStatusDto, + CreateShipmentDto, + UpdateShipmentDto, +} from './dto/shipment.dto'; +import { joinFullName } from '../common/utils/name'; +import { NotificationsService } from '../notifications/notifications.service'; +import { eventForStatus } from '../notifications/notification-events'; + +const STATUS_FLOW: ShipmentStatus[] = [ + ShipmentStatus.preparing, + ShipmentStatus.ready, + ShipmentStatus.shipped, + ShipmentStatus.delivered, +]; + +const SHIPMENT_TO_ORDER: Record = { + preparing: OrderStatus.preparing, + ready: OrderStatus.ready_to_ship, + shipped: OrderStatus.shipped, + delivered: OrderStatus.delivered, +}; + +@Injectable() +export class ShipmentsService { + constructor( + private readonly prisma: PrismaService, + private readonly notifications: NotificationsService, + ) {} + + async findAll(status?: ShipmentStatus) { + const shipments = await this.prisma.shipment.findMany({ + where: status ? { status } : undefined, + orderBy: { createdAt: 'desc' }, + include: { + order: { include: { user: true } }, + }, + }); + return shipments.map((s) => this.map(s)); + } + + async findOne(id: string) { + const shipment = await this.prisma.shipment.findUnique({ + where: { id }, + include: { order: { include: { user: true } } }, + }); + if (!shipment) throw new NotFoundException('ارسال یافت نشد'); + return this.map(shipment); + } + + async create(adminId: string, dto: CreateShipmentDto) { + const order = await this.prisma.order.findUnique({ + where: { id: dto.orderId }, + include: { shipment: true, user: true }, + }); + if (!order) throw new NotFoundException('سفارش یافت نشد'); + if (order.shipment) { + throw new ConflictException('برای این سفارش قبلاً ارسال ثبت شده است'); + } + + const status = dto.status ?? ShipmentStatus.preparing; + const shipment = await this.prisma.$transaction(async (tx) => { + const created = await tx.shipment.create({ + data: { + orderId: order.id, + carrier: dto.carrier.trim(), + trackingNumber: dto.trackingNumber?.trim() || null, + meterage: dto.meterage ?? order.totalMeterage, + status, + shippedAt: status === ShipmentStatus.shipped ? new Date() : null, + deliveredAt: status === ShipmentStatus.delivered ? new Date() : null, + }, + include: { order: { include: { user: true } } }, + }); + + const nextOrderStatus = SHIPMENT_TO_ORDER[status]; + if (order.status !== nextOrderStatus) { + await tx.order.update({ + where: { id: order.id }, + data: { status: nextOrderStatus }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: order.id, + fromStatus: order.status, + toStatus: nextOrderStatus, + changedBy: `admin:${adminId}`, + note: 'ثبت ارسال', + }, + }); + } + + return created; + }); + + this.notifyOrderStatus(order.id, SHIPMENT_TO_ORDER[status], { + carrier: shipment.carrier, + trackingNumber: shipment.trackingNumber, + }); + + return this.map(shipment); + } + + async update(id: string, dto: UpdateShipmentDto) { + await this.ensure(id); + const shipment = await this.prisma.shipment.update({ + where: { id }, + data: { + carrier: dto.carrier?.trim(), + trackingNumber: + dto.trackingNumber === undefined + ? undefined + : dto.trackingNumber?.trim() || null, + meterage: dto.meterage, + status: dto.status, + }, + include: { order: { include: { user: true } } }, + }); + return this.map(shipment); + } + + async advanceStatus( + id: string, + adminId: string, + dto: AdvanceShipmentStatusDto, + ) { + const shipment = await this.prisma.shipment.findUnique({ + where: { id }, + include: { order: { include: { user: true } } }, + }); + if (!shipment) throw new NotFoundException('ارسال یافت نشد'); + + let nextStatus = dto.status; + if (!nextStatus) { + const idx = STATUS_FLOW.indexOf(shipment.status); + if (idx < 0 || idx >= STATUS_FLOW.length - 1) { + throw new BadRequestException('وضعیت ارسال قابل پیشرفت نیست'); + } + nextStatus = STATUS_FLOW[idx + 1]!; + } + + const updated = await this.prisma.$transaction(async (tx) => { + const result = await tx.shipment.update({ + where: { id }, + data: { + status: nextStatus, + trackingNumber: + dto.trackingNumber !== undefined + ? dto.trackingNumber.trim() || null + : undefined, + shippedAt: + nextStatus === ShipmentStatus.shipped + ? (shipment.shippedAt ?? new Date()) + : shipment.shippedAt, + deliveredAt: + nextStatus === ShipmentStatus.delivered + ? new Date() + : shipment.deliveredAt, + }, + include: { order: { include: { user: true } } }, + }); + + const nextOrderStatus = SHIPMENT_TO_ORDER[nextStatus]; + if (shipment.order.status !== nextOrderStatus) { + await tx.order.update({ + where: { id: shipment.orderId }, + data: { status: nextOrderStatus }, + }); + await tx.orderStatusHistory.create({ + data: { + orderId: shipment.orderId, + fromStatus: shipment.order.status, + toStatus: nextOrderStatus, + changedBy: `admin:${adminId}`, + note: `وضعیت ارسال: ${nextStatus}`, + }, + }); + } + + return result; + }); + + this.notifyOrderStatus(shipment.orderId, SHIPMENT_TO_ORDER[nextStatus], { + carrier: updated.carrier, + trackingNumber: updated.trackingNumber, + }); + + return this.map(updated); + } + + private notifyOrderStatus( + orderId: string, + status: OrderStatus, + context: { carrier?: string | null; trackingNumber?: string | null }, + ) { + const event = eventForStatus(status); + if (event) { + this.notifications.dispatchOrderEvent(orderId, event, context); + } + } + + private async ensure(id: string) { + const shipment = await this.prisma.shipment.findUnique({ where: { id } }); + if (!shipment) throw new NotFoundException('ارسال یافت نشد'); + return shipment; + } + + private map(s: { + id: string; + orderId: string; + carrier: string; + trackingNumber: string | null; + meterage: number; + status: ShipmentStatus; + shippedAt: Date | null; + deliveredAt: Date | null; + createdAt: Date; + updatedAt: Date; + order: { + orderNumber: string; + user: { firstName: string; lastName: string }; + }; + }) { + return { + id: s.id, + orderId: s.orderId, + orderNumber: s.order.orderNumber, + customerName: joinFullName(s.order.user.firstName, s.order.user.lastName), + carrier: s.carrier, + trackingNumber: s.trackingNumber ?? '', + meterage: s.meterage, + status: s.status, + shippedAt: s.shippedAt?.toISOString() ?? null, + deliveredAt: s.deliveredAt?.toISOString() ?? null, + createdAt: s.createdAt.toISOString(), + updatedAt: s.updatedAt.toISOString(), + }; + } +} diff --git a/src/sms/sms.module.ts b/src/sms/sms.module.ts new file mode 100644 index 0000000..cc282dd --- /dev/null +++ b/src/sms/sms.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SmsService } from './sms.service'; + +@Module({ + providers: [SmsService], + exports: [SmsService], +}) +export class SmsModule {} diff --git a/src/sms/sms.service.ts b/src/sms/sms.service.ts new file mode 100644 index 0000000..3872d68 --- /dev/null +++ b/src/sms/sms.service.ts @@ -0,0 +1,109 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export type SmsSendResult = { + success: boolean; + raw: unknown; +}; + +function maskPhone(phone: string): string { + return phone.length <= 4 + ? '****' + : `${phone.slice(0, 4)}****${phone.slice(-2)}`; +} + +@Injectable() +export class SmsService { + private readonly logger = new Logger(SmsService.name); + + constructor(private readonly config: ConfigService) {} + + isEnabled(): boolean { + const raw = ( + this.config.get('SMS_ENABLED', 'true') ?? 'true' + ).toLowerCase(); + return raw === '1' || raw === 'true' || raw === 'yes'; + } + + async send(to: string, message: string): Promise { + const url = this.config.get( + 'SMS_API_URL', + 'http://185.231.112.101/api/createsms', + ); + const secretKey = this.config.get('SMS_SECRET_KEY')?.trim(); + if (!secretKey) { + this.logger.error('SMS_SECRET_KEY تنظیم نشده است'); + return { success: false, raw: 'SMS_SECRET_KEY missing' }; + } + const platform = this.config.get('SMS_PLATFORM', 'rayegansms'); + const host = this.resolveHost(); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + secret_key: secretKey, + }, + body: JSON.stringify({ + phone: to, + host, + text: message.trim(), + platform, + }), + signal: AbortSignal.timeout(15_000), + }); + + const text = await response.text(); + let body: unknown = text; + try { + body = text ? JSON.parse(text) : null; + } catch { + /* keep raw text */ + } + + let success = response.ok; + if (success && body && typeof body === 'object') { + const obj = body as Record; + if ('success' in obj && obj.success === false) success = false; + else if ('ok' in obj && obj.ok === false) success = false; + else if ( + typeof obj.status === 'string' && + ['error', 'failed', 'fail'].includes(obj.status.toLowerCase()) + ) { + success = false; + } + } + + if (!success) { + this.logger.warn( + `SMS failed to ${maskPhone(to)}: ${JSON.stringify(body)}`, + ); + } + + return { success, raw: body }; + } catch (err) { + this.logger.error( + `SMS request error to ${maskPhone(to)}`, + err instanceof Error ? err.stack : err, + ); + return { + success: false, + raw: err instanceof Error ? err.message : String(err), + }; + } + } + + buildOtpMessage(code: string): string { + const appName = this.config.get('SMS_APP_NAME', 'پارچه'); + return `رمز عبور یکبار مصرف شما جهت ورود به اپلیکیشن ${appName} ${code} می باشد`; + } + + private resolveHost(): string { + const fromEnv = + this.config.get('SMS_HOST')?.trim() || + this.config.get('SMS_APP_NAME')?.trim(); + if (fromEnv) return fromEnv; + return 'fabric'; + } +} diff --git a/src/stock-notify/stock-notify.controller.ts b/src/stock-notify/stock-notify.controller.ts new file mode 100644 index 0000000..ede9ba6 --- /dev/null +++ b/src/stock-notify/stock-notify.controller.ts @@ -0,0 +1,44 @@ +import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { IsString, MinLength } from 'class-validator'; +import { StockNotifyService } from './stock-notify.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; + +class SubscribeDto { + @IsString() + @MinLength(1) + productId!: string; +} + +@Controller() +export class StockNotifyController { + constructor(private readonly stockNotifyService: StockNotifyService) {} + + @AuthType('admin') + @Get('admin/stock-alerts') + list(@Query('activeOnly') activeOnly?: string) { + return this.stockNotifyService.listAdmin(activeOnly !== 'false'); + } + + @AuthType('admin') + @Patch('admin/stock-alerts/:id/notify') + notify(@Param('id') id: string) { + return this.stockNotifyService.markNotified(id); + } + + @AuthType('customer') + @Post('stock-alerts') + subscribe(@CurrentUser() user: AuthPrincipal, @Body() dto: SubscribeDto) { + return this.stockNotifyService.subscribe(user.id, dto.productId); + } + + @AuthType('customer') + @Post('stock-alerts/:productId/cancel') + unsubscribe( + @CurrentUser() user: AuthPrincipal, + @Param('productId') productId: string, + ) { + return this.stockNotifyService.unsubscribe(user.id, productId); + } +} diff --git a/src/stock-notify/stock-notify.module.ts b/src/stock-notify/stock-notify.module.ts new file mode 100644 index 0000000..38a0d1a --- /dev/null +++ b/src/stock-notify/stock-notify.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { StockNotifyController } from './stock-notify.controller'; +import { StockNotifyService } from './stock-notify.service'; + +@Module({ + controllers: [StockNotifyController], + providers: [StockNotifyService], + exports: [StockNotifyService], +}) +export class StockNotifyModule {} diff --git a/src/stock-notify/stock-notify.service.ts b/src/stock-notify/stock-notify.service.ts new file mode 100644 index 0000000..f6d21b2 --- /dev/null +++ b/src/stock-notify/stock-notify.service.ts @@ -0,0 +1,96 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class StockNotifyService { + constructor(private readonly prisma: PrismaService) {} + + async listAdmin(activeOnly = true) { + const rows = await this.prisma.stockNotifyRequest.findMany({ + where: activeOnly ? { active: true } : undefined, + orderBy: { createdAt: 'desc' }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true, stock: true } }, + }, + }); + return rows.map((r) => this.map(r)); + } + + async subscribe(customerId: string, productId: string) { + const product = await this.prisma.product.findUnique({ where: { id: productId } }); + if (!product) throw new NotFoundException('محصول یافت نشد'); + const row = await this.prisma.stockNotifyRequest.upsert({ + where: { customerId_productId: { customerId, productId } }, + create: { customerId, productId, active: true }, + update: { active: true, notifiedAt: null }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true, stock: true } }, + }, + }); + return this.map(row); + } + + async unsubscribe(customerId: string, productId: string) { + await this.prisma.stockNotifyRequest.updateMany({ + where: { customerId, productId }, + data: { active: false }, + }); + return { productId, active: false }; + } + + async markNotified(id: string) { + const existing = await this.prisma.stockNotifyRequest.findUnique({ + where: { id }, + }); + if (!existing) throw new NotFoundException('درخواست یافت نشد'); + const row = await this.prisma.stockNotifyRequest.update({ + where: { id }, + data: { notifiedAt: new Date(), active: false }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true, stock: true } }, + }, + }); + + await this.prisma.customerNotification.create({ + data: { + customerId: row.customerId, + event: 'stock_available', + title: 'موجودی محصول', + body: `محصول ${row.product.name} دوباره موجود شد.`, + tone: 'success', + link: `/product?id=${row.productId}`, + }, + }); + + return this.map(row); + } + + private map(r: { + id: string; + customerId: string; + productId: string; + active: boolean; + createdAt: Date; + notifiedAt: Date | null; + customer: { firstName: string; lastName: string; phone: string }; + product: { name: string; code: string; color: string; stock: number | null }; + }) { + return { + id: r.id, + customerId: r.customerId, + customerName: `${r.customer.firstName} ${r.customer.lastName}`.trim(), + customerPhone: r.customer.phone, + productId: r.productId, + productName: r.product.name, + productCode: r.product.code, + color: r.product.color, + stock: r.product.stock, + active: r.active, + createdAt: r.createdAt.toISOString(), + notifiedAt: r.notifiedAt?.toISOString(), + }; + } +} diff --git a/src/suggestions/suggestions.controller.ts b/src/suggestions/suggestions.controller.ts new file mode 100644 index 0000000..1b63a4f --- /dev/null +++ b/src/suggestions/suggestions.controller.ts @@ -0,0 +1,60 @@ +import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { IsBoolean, IsOptional, IsString, MinLength } from 'class-validator'; +import { SuggestionsService } from './suggestions.service'; +import { AuthType } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthPrincipal } from '../common/decorators/current-user.decorator'; + +class CreateSuggestionDto { + @IsString() + @MinLength(1) + customerId!: string; + + @IsString() + @MinLength(1) + productId!: string; + + @IsOptional() + @IsString() + note?: string; +} + +class SetActiveDto { + @IsBoolean() + isActive!: boolean; +} + +@Controller() +export class SuggestionsController { + constructor(private readonly suggestionsService: SuggestionsService) {} + + @AuthType('admin') + @Get('admin/suggestions') + list(@Query('customerId') customerId?: string) { + return this.suggestionsService.listAdmin(customerId); + } + + @AuthType('admin') + @Post('admin/suggestions') + create( + @CurrentUser() user: AuthPrincipal, + @Body() dto: CreateSuggestionDto, + ) { + return this.suggestionsService.create({ + ...dto, + adminId: user.id, + }); + } + + @AuthType('admin') + @Patch('admin/suggestions/:id/active') + setActive(@Param('id') id: string, @Body() dto: SetActiveDto) { + return this.suggestionsService.setActive(id, dto.isActive); + } + + @AuthType('customer') + @Get('suggestions/me') + mine(@CurrentUser() user: AuthPrincipal) { + return this.suggestionsService.listForCustomer(user.id); + } +} diff --git a/src/suggestions/suggestions.module.ts b/src/suggestions/suggestions.module.ts new file mode 100644 index 0000000..687d525 --- /dev/null +++ b/src/suggestions/suggestions.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SuggestionsController } from './suggestions.controller'; +import { SuggestionsService } from './suggestions.service'; + +@Module({ + controllers: [SuggestionsController], + providers: [SuggestionsService], + exports: [SuggestionsService], +}) +export class SuggestionsModule {} diff --git a/src/suggestions/suggestions.service.ts b/src/suggestions/suggestions.service.ts new file mode 100644 index 0000000..7be675e --- /dev/null +++ b/src/suggestions/suggestions.service.ts @@ -0,0 +1,108 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class SuggestionsService { + constructor(private readonly prisma: PrismaService) {} + + async listAdmin(customerId?: string) { + const rows = await this.prisma.productSuggestion.findMany({ + where: { + ...(customerId ? { customerId } : {}), + }, + orderBy: { createdAt: 'desc' }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true } }, + }, + }); + return rows.map((r) => this.map(r)); + } + + async listForCustomer(customerId: string) { + const rows = await this.prisma.productSuggestion.findMany({ + where: { customerId, isActive: true }, + orderBy: { createdAt: 'desc' }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true } }, + }, + }); + return rows.map((r) => this.map(r)); + } + + async create(input: { + customerId: string; + productId: string; + note?: string; + adminId?: string; + }) { + await this.ensureCustomer(input.customerId); + await this.ensureProduct(input.productId); + const row = await this.prisma.productSuggestion.create({ + data: { + customerId: input.customerId, + productId: input.productId, + note: input.note?.trim() || null, + createdByAdminId: input.adminId, + isActive: true, + }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true } }, + }, + }); + return this.map(row); + } + + async setActive(id: string, isActive: boolean) { + const existing = await this.prisma.productSuggestion.findUnique({ + where: { id }, + }); + if (!existing) throw new NotFoundException('پیشنهاد یافت نشد'); + const row = await this.prisma.productSuggestion.update({ + where: { id }, + data: { isActive }, + include: { + customer: { select: { firstName: true, lastName: true, phone: true } }, + product: { select: { name: true, code: true, color: true } }, + }, + }); + return this.map(row); + } + + private async ensureCustomer(id: string) { + const c = await this.prisma.customer.findUnique({ where: { id } }); + if (!c) throw new NotFoundException('مشتری یافت نشد'); + } + + private async ensureProduct(id: string) { + const p = await this.prisma.product.findUnique({ where: { id } }); + if (!p) throw new NotFoundException('محصول یافت نشد'); + } + + private map(r: { + id: string; + customerId: string; + productId: string; + note: string | null; + isActive: boolean; + createdAt: Date; + customer: { firstName: string; lastName: string; phone: string }; + product: { name: string; code: string; color: string }; + }) { + return { + id: r.id, + customerId: r.customerId, + customerName: `${r.customer.firstName} ${r.customer.lastName}`.trim(), + customerPhone: r.customer.phone, + productId: r.productId, + productName: r.product.name, + productCode: r.product.code, + color: r.product.color, + note: r.note ?? undefined, + isActive: r.isActive, + createdAt: r.createdAt.toISOString(), + }; + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100644 index 0000000..a767839 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,29 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..701e87e --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..57f9635 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true + } +}