init
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
name: deploy to danak
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DANAK_SERVER: "https://captain.dev.danakcorp.com"
|
||||
APP_TOKEN: e7d4e8ae6b8164d3c1669c161f7cab295549b6fdfaea90eaf833f24122380841
|
||||
APP_NAME: dmenuplus-api-production
|
||||
GITHUB_TOKEN: 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
|
||||
|
||||
steps:
|
||||
- name: Check out repositorys
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: zmihamid
|
||||
password: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
- name: Preset Image Name
|
||||
run: echo "IMAGE_URL=$(echo ghcr.io/zmihamid/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_URL }}
|
||||
|
||||
- name: Install CapRover CLI
|
||||
run: npm install -g caprover
|
||||
|
||||
- name: deploy to server
|
||||
run: |
|
||||
caprover deploy -a $APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL"
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# 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 and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
/react/*
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"insertPragma": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"eslint.format.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Stage 1: Base
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Install timezone support
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
|
||||
# Set workdir for clarity
|
||||
WORKDIR /app
|
||||
|
||||
# Stage 2: Dependencies
|
||||
FROM base AS deps
|
||||
WORKDIR /temp-deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Stage 3: Build
|
||||
FROM base AS builder
|
||||
WORKDIR /build
|
||||
COPY . ./
|
||||
COPY --from=deps /temp-deps/node_modules ./node_modules
|
||||
RUN if [ -f package.json ] && grep -q '"build":' package.json; then npm run build; fi
|
||||
|
||||
# Stage 4: Runner
|
||||
FROM base AS runner
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
COPY --from=builder --chown=appuser:appgroup /build/ ./
|
||||
RUN chown -R appuser:appgroup /app
|
||||
|
||||
USER appuser
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
# # Stage 1: Build Stage
|
||||
# FROM node:22-alpine AS base
|
||||
|
||||
# RUN npm install -g corepack@latest
|
||||
# RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# # Install tzdata to support timezone settings
|
||||
# RUN apk add --no-cache tzdata
|
||||
|
||||
# # Set the timezone to Asia/Tehran
|
||||
# RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
|
||||
|
||||
# FROM base AS deps
|
||||
# WORKDIR /temp-deps
|
||||
# COPY package*.json pnpm-lock.yaml ./
|
||||
# RUN pnpm install --frozen-lockfile --loglevel info
|
||||
|
||||
|
||||
# FROM base AS builder
|
||||
# WORKDIR /build
|
||||
# COPY . ./
|
||||
# COPY --from=deps /temp-deps/node_modules ./node_modules
|
||||
# RUN if [ -f package.json ] && grep -q '"build":' package.json; then pnpm run build; fi
|
||||
# RUN pnpm install --prod --frozen-lockfile --loglevel info
|
||||
|
||||
# FROM base AS runner
|
||||
# RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
# WORKDIR /app
|
||||
|
||||
# COPY . ./
|
||||
# COPY --from=builder --chown=appuser:appgroup /build/ ./
|
||||
|
||||
# RUN chown -R appuser:appgroup /app
|
||||
|
||||
# USER appuser
|
||||
|
||||
# ENV NODE_ENV=production
|
||||
# EXPOSE 3000
|
||||
|
||||
# CMD ["npm", "start"]
|
||||
@@ -0,0 +1,98 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260105085439 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
|
||||
|
||||
this.addSql(`alter table "categories" add column "order" int null;`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`create table "user_wallets" ("id" char(26) not null, "created_at" timestamptz(6) not null default now(), "updated_at" timestamptz(6) not null default now(), "deleted_at" timestamptz(6) null, "restaurant_id" char(26) not null, "user_id" char(26) not null, "wallet" int4 not null default 0, "points" int4 not null default 0, constraint "user_wallets_pkey" primary key ("id"));`);
|
||||
this.addSql(`alter table "user_wallets" add constraint "unique_user_restaurant" unique ("user_id", "restaurant_id");`);
|
||||
this.addSql(`create index "user_wallets_created_at_index" on "user_wallets" ("created_at");`);
|
||||
this.addSql(`create index "user_wallets_deleted_at_index" on "user_wallets" ("deleted_at");`);
|
||||
this.addSql(`create index "user_wallets_restaurant_id_index" on "user_wallets" ("restaurant_id");`);
|
||||
this.addSql(`create index "user_wallets_user_id_index" on "user_wallets" ("user_id");`);
|
||||
this.addSql(`create index "user_wallets_user_id_restaurant_id_index" on "user_wallets" ("user_id", "restaurant_id");`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" drop constraint if exists "point_transactions_type_check";`);
|
||||
|
||||
this.addSql(`alter table "point_transactions" add constraint "point_transactions_type_check" check("type" in (''));`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260106054151 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" drop not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" drop not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" drop not null;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" type varchar(255) using ("subscription_id"::varchar(255));`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_id" set not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" type timestamptz using ("subscription_end_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_end_date" set not null;`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" type timestamptz using ("subscription_start_date"::timestamptz);`);
|
||||
this.addSql(`alter table "restaurants" alter column "subscription_start_date" set not null;`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260106192946 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "contacts" add column "restaurant_id" char(26) null;`);
|
||||
this.addSql(`alter table "contacts" add constraint "contacts_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurants" ("id") on update cascade on delete set null;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "contacts" drop constraint "contacts_restaurant_id_foreign";`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20260226063544 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "deliveries" alter column "delivery_fee" type int using ("delivery_fee"::int);`);
|
||||
this.addSql(`alter table "deliveries" alter column "per_kilometer_fee" type int using ("per_kilometer_fee"::int);`);
|
||||
this.addSql(`alter table "deliveries" alter column "distance_based_min_cost" type int using ("distance_based_min_cost"::int);`);
|
||||
this.addSql(`alter table "deliveries" alter column "min_order_price" type int using ("min_order_price"::int);`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "deliveries" alter column "delivery_fee" type numeric(10,0) using ("delivery_fee"::numeric(10,0));`);
|
||||
this.addSql(`alter table "deliveries" alter column "per_kilometer_fee" type numeric(10,0) using ("per_kilometer_fee"::numeric(10,0));`);
|
||||
this.addSql(`alter table "deliveries" alter column "distance_based_min_cost" type numeric(10,0) using ("distance_based_min_cost"::numeric(10,0));`);
|
||||
this.addSql(`alter table "deliveries" alter column "min_order_price" type numeric(10,0) using ("min_order_price"::numeric(10,0));`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Icon Naming Process
|
||||
|
||||
## Files Created:
|
||||
- `icons.csv` - Updated with "icon_name" column
|
||||
- `icon_viewer.html` - Web interface to view and name icons
|
||||
- `populate_icon_names.js` - Script to add names to CSV
|
||||
|
||||
## Step-by-Step Process:
|
||||
|
||||
### 1. View the Icons
|
||||
- Open `icon_viewer.html` in your web browser
|
||||
- You'll see all 138 icons in a grid layout
|
||||
- Each icon shows its number and has an input field for naming
|
||||
|
||||
### 2. Name the Icons
|
||||
- Look at each icon and enter appropriate names in the input fields
|
||||
- The progress counter shows how many you've named
|
||||
- Names should be descriptive (e.g., "hamburger", "pizza", "coffee", "delivery")
|
||||
|
||||
### 3. Export Named Icons (Optional)
|
||||
- Click "Export Named Icons" to download a CSV of only the named icons
|
||||
- This helps you review your naming before finalizing
|
||||
|
||||
### 4. Update the Main CSV
|
||||
- Edit `populate_icon_names.js`
|
||||
- Replace the placeholder names in the `iconNames` array with your actual names
|
||||
- Run the script: `node populate_icon_names.js`
|
||||
|
||||
## Example Icon Names:
|
||||
- Food items: "pizza", "burger", "pasta", "salad"
|
||||
- Drinks: "coffee", "soda", "juice", "beer"
|
||||
- Categories: "fast_food", "italian", "asian", "desserts"
|
||||
- Actions: "delivery", "pickup", "favorite", "cart"
|
||||
|
||||
## Tips:
|
||||
- Use consistent naming conventions (snake_case or camelCase)
|
||||
- Keep names short but descriptive
|
||||
- Group similar icons with consistent prefixes if applicable
|
||||
- Review all names before running the populate script
|
||||
|
||||
The final CSV will have all original columns plus the new "icon_name" column with your custom names.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to dump icon and icon_groups tables from PostgreSQL database
|
||||
# Usage: ./dump-icons.sh [output_file]
|
||||
|
||||
set -e
|
||||
|
||||
# Load environment variables if .env file exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# Source .env file, handling comments and empty lines
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Remove leading/trailing whitespace
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
# Skip empty lines and lines starting with #
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
# Remove inline comments (everything after # that's not in quotes)
|
||||
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
|
||||
# Skip if line is empty after removing comments
|
||||
[[ -z "$line" ]] && continue
|
||||
# Export the variable (only if it looks like KEY=VALUE)
|
||||
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
|
||||
export "$line" 2>/dev/null || true
|
||||
fi
|
||||
done < .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get database connection details from environment variables
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-dmenu}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
|
||||
# Output file name (default: icons_dump_YYYYMMDD_HHMMSS.sql)
|
||||
if [ -z "$1" ]; then
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
OUTPUT_FILE="icons_dump_${TIMESTAMP}.sql"
|
||||
else
|
||||
OUTPUT_FILE="$1"
|
||||
fi
|
||||
|
||||
echo "Dumping icon_groups and icons tables..."
|
||||
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
|
||||
echo "Output file: ${OUTPUT_FILE}"
|
||||
|
||||
# Dump icon_groups table first (parent table)
|
||||
echo "Dumping icon_groups table..."
|
||||
PGPASSWORD="${DB_PASS}" pg_dump \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-t icon_groups \
|
||||
--data-only \
|
||||
--column-inserts \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
> "${OUTPUT_FILE}"
|
||||
|
||||
# Append icons table to the dump file
|
||||
echo "Dumping icons table..."
|
||||
PGPASSWORD="${DB_PASS}" pg_dump \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-t icons \
|
||||
--data-only \
|
||||
--column-inserts \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
>> "${OUTPUT_FILE}"
|
||||
|
||||
echo "Dump completed successfully!"
|
||||
echo "File saved to: ${OUTPUT_FILE}"
|
||||
echo ""
|
||||
echo "To restore, run: ./restore-icons.sh ${OUTPUT_FILE}"
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Icon Viewer - Name the Icons</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.header {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
.icon-item {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.icon-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.icon-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.icon-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.icon-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.save-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.progress {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Icon Name Editor</h1>
|
||||
<p>Review each icon below and enter appropriate names. Names will be saved back to the CSV file.</p>
|
||||
<div class="progress">
|
||||
<span id="completed-count">0</span> of <span id="total-count">138</span> icons named
|
||||
</div>
|
||||
<button class="save-btn" onclick="saveToCSV()">Save Names to CSV</button>
|
||||
<button class="save-btn" onclick="exportNamedIcons()" style="margin-left: 10px; background-color: #28a745;">Export Named Icons</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-grid" id="iconGrid">
|
||||
<!-- Icons will be loaded here -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const icons = [
|
||||
"https://storage.danakcorp.com/images/1766554365832-411750172.svg",
|
||||
"https://storage.danakcorp.com/images/1766554379149-104970938.svg",
|
||||
"https://storage.danakcorp.com/images/1766554389090-593863597.svg",
|
||||
"https://storage.danakcorp.com/images/1766554404968-905984091.svg",
|
||||
"https://storage.danakcorp.com/images/1766554415325-873800591.svg",
|
||||
"https://storage.danakcorp.com/images/1766554428814-77027818.svg",
|
||||
"https://storage.danakcorp.com/images/1766554438632-45072052.svg",
|
||||
"https://storage.danakcorp.com/images/1766554446340-463157109.svg",
|
||||
"https://storage.danakcorp.com/images/1766554456605-280166176.svg",
|
||||
"https://storage.danakcorp.com/images/1766554471887-315640495.svg",
|
||||
"https://storage.danakcorp.com/images/1766554485634-242317100.svg",
|
||||
"https://storage.danakcorp.com/images/1766554504553-157481346.svg",
|
||||
"https://storage.danakcorp.com/images/1766554528198-919877647.svg",
|
||||
"https://storage.danakcorp.com/images/1766554544140-886744157.svg",
|
||||
"https://storage.danakcorp.com/images/1766554565614-684869010.svg",
|
||||
"https://storage.danakcorp.com/images/1766554578855-318583145.svg",
|
||||
"https://storage.danakcorp.com/images/1766554593272-362758712.svg",
|
||||
"https://storage.danakcorp.com/images/1766554606166-877198688.svg",
|
||||
"https://storage.danakcorp.com/images/1766554624133-21439996.svg",
|
||||
"https://storage.danakcorp.com/images/1766554641691-535696200.svg",
|
||||
"https://storage.danakcorp.com/images/1766554658481-114730426.svg",
|
||||
"https://storage.danakcorp.com/images/1766554671760-634916570.svg",
|
||||
"https://storage.danakcorp.com/images/1766554967381-660612838.svg",
|
||||
"https://storage.danakcorp.com/images/1766555106925-267924006.svg",
|
||||
"https://storage.danakcorp.com/images/1766555231674-387319393.svg",
|
||||
"https://storage.danakcorp.com/images/1766555290519-155637622.svg",
|
||||
"https://storage.danakcorp.com/images/1766555308301-230359494.svg",
|
||||
"https://storage.danakcorp.com/images/1766555420851-186440015.svg",
|
||||
"https://storage.danakcorp.com/images/1766555590828-644527730.svg",
|
||||
"https://storage.danakcorp.com/images/1766555606997-571682462.svg",
|
||||
"https://storage.danakcorp.com/images/1766555663659-433052581.svg",
|
||||
"https://storage.danakcorp.com/images/1766555713098-988242030.svg",
|
||||
"https://storage.danakcorp.com/images/1766555726158-680948882.svg",
|
||||
"https://storage.danakcorp.com/images/1766555746650-426516324.svg",
|
||||
"https://storage.danakcorp.com/images/1766555765798-127083646.svg",
|
||||
"https://storage.danakcorp.com/images/1766555784837-243715698.svg",
|
||||
"https://storage.danakcorp.com/images/1766555799191-594035927.svg",
|
||||
"https://storage.danakcorp.com/images/1766555894624-524350777.svg",
|
||||
"https://storage.danakcorp.com/images/1766555932114-485954688.svg",
|
||||
"https://storage.danakcorp.com/images/1766555949650-909996041.svg",
|
||||
"https://storage.danakcorp.com/images/1766555966861-75517122.svg",
|
||||
"https://storage.danakcorp.com/images/1766556006875-239689681.svg",
|
||||
"https://storage.danakcorp.com/images/1766556257163-459958715.svg",
|
||||
"https://storage.danakcorp.com/images/1766556293812-665835248.svg",
|
||||
"https://storage.danakcorp.com/images/1766556307082-162280793.svg",
|
||||
"https://storage.danakcorp.com/images/1766556340402-744632392.svg",
|
||||
"https://storage.danakcorp.com/images/1766556458335-857124841.svg",
|
||||
"https://storage.danakcorp.com/images/1766556479530-161860621.svg",
|
||||
"https://storage.danakcorp.com/images/1766556498215-91175544.svg",
|
||||
"https://storage.danakcorp.com/images/1766556677355-108854669.svg",
|
||||
"https://storage.danakcorp.com/images/1766556736283-691587656.svg",
|
||||
"https://storage.danakcorp.com/images/1766556759367-58066921.svg",
|
||||
"https://storage.danakcorp.com/images/1766556777353-490756125.svg",
|
||||
"https://storage.danakcorp.com/images/1766556870740-118553581.svg",
|
||||
"https://storage.danakcorp.com/images/1766556890334-962678132.svg",
|
||||
"https://storage.danakcorp.com/images/1766556921029-865260745.svg",
|
||||
"https://storage.danakcorp.com/images/1766556946363-320833501.svg",
|
||||
"https://storage.danakcorp.com/images/1766556973771-832565827.svg",
|
||||
"https://storage.danakcorp.com/images/1766557056358-727862983.svg",
|
||||
"https://storage.danakcorp.com/images/1766557199259-72955190.svg",
|
||||
"https://storage.danakcorp.com/images/1766557252452-988016525.svg",
|
||||
"https://storage.danakcorp.com/images/1766557274778-439028456.svg",
|
||||
"https://storage.danakcorp.com/images/1766557289094-415104854.svg",
|
||||
"https://storage.danakcorp.com/images/1766557300923-302277606.svg",
|
||||
"https://storage.danakcorp.com/images/1766557317091-602116315.svg",
|
||||
"https://storage.danakcorp.com/images/1766557647999-47333939.svg",
|
||||
"https://storage.danakcorp.com/images/1766557669282-172512141.svg",
|
||||
"https://storage.danakcorp.com/images/1766557705283-692546793.svg",
|
||||
"https://storage.danakcorp.com/images/1766557729175-762576743.svg",
|
||||
"https://storage.danakcorp.com/images/1766557756487-665300947.svg",
|
||||
"https://storage.danakcorp.com/images/1766557776067-804149228.svg",
|
||||
"https://storage.danakcorp.com/images/1766557797509-101726352.svg",
|
||||
"https://storage.danakcorp.com/images/1766557816484-357699340.svg",
|
||||
"https://storage.danakcorp.com/images/1766557837292-326521286.svg",
|
||||
"https://storage.danakcorp.com/images/1766559083575-564721059.svg",
|
||||
"https://storage.danakcorp.com/images/1766559099561-720966516.svg",
|
||||
"https://storage.danakcorp.com/images/1766559116496-834964990.svg",
|
||||
"https://storage.danakcorp.com/images/1766563021426-600579018.svg",
|
||||
"https://storage.danakcorp.com/images/1766563038525-727531190.svg",
|
||||
"https://storage.danakcorp.com/images/1766563051485-824602001.svg",
|
||||
"https://storage.danakcorp.com/images/1766563066017-372210647.svg",
|
||||
"https://storage.danakcorp.com/images/1766563124296-331607800.svg",
|
||||
"https://storage.danakcorp.com/images/1766563142133-359920423.svg",
|
||||
"https://storage.danakcorp.com/images/1766563164270-350101986.svg",
|
||||
"https://storage.danakcorp.com/images/1766563181587-86630830.svg",
|
||||
"https://storage.danakcorp.com/images/1766563204966-651499380.svg",
|
||||
"https://storage.danakcorp.com/images/1766563220596-103699525.svg",
|
||||
"https://storage.danakcorp.com/images/1766563265634-373953449.svg",
|
||||
"https://storage.danakcorp.com/images/1766563275517-958045207.svg",
|
||||
"https://storage.danakcorp.com/images/1766563286526-622849562.svg",
|
||||
"https://storage.danakcorp.com/images/1766564695339-504553420.svg",
|
||||
"https://storage.danakcorp.com/images/1766564706032-159367016.svg",
|
||||
"https://storage.danakcorp.com/images/1766564720683-887818252.svg",
|
||||
"https://storage.danakcorp.com/images/1766564734432-483528219.svg",
|
||||
"https://storage.danakcorp.com/images/1766564750099-854146988.svg",
|
||||
"https://storage.danakcorp.com/images/1766564758573-696060786.svg",
|
||||
"https://storage.danakcorp.com/images/1766564805823-829938380.svg",
|
||||
"https://storage.danakcorp.com/images/1766564891137-468827500.svg",
|
||||
"https://storage.danakcorp.com/images/1766564907215-921380792.svg",
|
||||
"https://storage.danakcorp.com/images/1766564926872-672612697.svg",
|
||||
"https://storage.danakcorp.com/images/1766565459604-638026702.svg",
|
||||
"https://storage.danakcorp.com/images/1766565476771-42527917.svg",
|
||||
"https://storage.danakcorp.com/images/1766565491264-946731491.svg",
|
||||
"https://storage.danakcorp.com/images/1766565502475-288189136.svg",
|
||||
"https://storage.danakcorp.com/images/1766565530628-800474580.svg",
|
||||
"https://storage.danakcorp.com/images/1766565547983-878658938.svg",
|
||||
"https://storage.danakcorp.com/images/1766565563263-891688321.svg",
|
||||
"https://storage.danakcorp.com/images/1766565600547-704332792.svg",
|
||||
"https://storage.danakcorp.com/images/1766565625437-528383713.svg",
|
||||
"https://storage.danakcorp.com/images/1766565641620-53176909.svg",
|
||||
"https://storage.danakcorp.com/images/1766565664427-433669317.svg",
|
||||
"https://storage.danakcorp.com/images/1766565971515-159903323.svg",
|
||||
"https://storage.danakcorp.com/images/1766565995424-288101561.svg",
|
||||
"https://storage.danakcorp.com/images/1766566026942-868807643.svg",
|
||||
"https://storage.danakcorp.com/images/1766566041525-973019175.svg",
|
||||
"https://storage.danakcorp.com/images/1766566064741-271844433.svg",
|
||||
"https://storage.danakcorp.com/images/1766566084368-438871956.svg",
|
||||
"https://storage.danakcorp.com/images/1766566106954-47179607.svg",
|
||||
"https://storage.danakcorp.com/images/1766566122836-486871124.svg",
|
||||
"https://storage.danakcorp.com/images/1766566143171-668309076.svg",
|
||||
"https://storage.danakcorp.com/images/1766566161551-844376214.svg",
|
||||
"https://storage.danakcorp.com/images/1766566176755-30806596.svg",
|
||||
"https://storage.danakcorp.com/images/1766566193720-339416210.svg",
|
||||
"https://storage.danakcorp.com/images/1766566293736-981836756.svg",
|
||||
"https://storage.danakcorp.com/images/1766566304318-193301989.svg",
|
||||
"https://storage.danakcorp.com/images/1766566327768-2675508.svg",
|
||||
"https://storage.danakcorp.com/images/1766566341717-462491426.svg",
|
||||
"https://storage.danakcorp.com/images/1766566357028-432111036.svg",
|
||||
"https://storage.danakcorp.com/images/1766566371354-279596850.svg",
|
||||
"https://storage.danakcorp.com/images/1766566385674-569493499.svg",
|
||||
"https://storage.danakcorp.com/images/1766566413058-975642584.svg",
|
||||
"https://storage.danakcorp.com/images/1766566427440-982841441.svg",
|
||||
"https://storage.danakcorp.com/images/1766566442808-761588425.svg",
|
||||
"https://storage.danakcorp.com/images/1766566468108-519151895.svg",
|
||||
"https://storage.danakcorp.com/images/1766566482278-661272876.svg",
|
||||
"https://storage.danakcorp.com/images/1766566494050-408184442.svg",
|
||||
"https://storage.danakcorp.com/images/1766566504760-709260995.svg"
|
||||
];
|
||||
|
||||
const iconNames = new Array(icons.length).fill('');
|
||||
|
||||
function loadIcons() {
|
||||
const grid = document.getElementById('iconGrid');
|
||||
icons.forEach((url, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'icon-item';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="icon-id">Icon ${index + 1}</div>
|
||||
<img src="${url}" alt="Icon ${index + 1}" class="icon-img" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiBmaWxsPSIjZGRkIi8+Cjx0ZXh0IHg9IjI0IiB5PSIyNCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zNWVtIj5ObyBJY29uPC90ZXh0Pgo8L3N2Zz4='">
|
||||
<input type="text" class="input-field" placeholder="Enter icon name..." oninput="updateName(${index}, this.value)">
|
||||
`;
|
||||
|
||||
grid.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateName(index, name) {
|
||||
iconNames[index] = name.trim();
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const completed = iconNames.filter(name => name.length > 0).length;
|
||||
document.getElementById('completed-count').textContent = completed;
|
||||
}
|
||||
|
||||
function saveToCSV() {
|
||||
// This will create a downloadable CSV with the names
|
||||
let csvContent = '"id","created_at","updated_at","deleted_at","url","group_id","icon_name"\n';
|
||||
|
||||
// You'll need to load the original CSV data here and add the names
|
||||
// For now, this is a placeholder - you'd need to combine with original data
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'icons_with_names.csv';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
alert('CSV template downloaded. You\'ll need to manually add the icon names to the original CSV file.');
|
||||
}
|
||||
|
||||
function exportNamedIcons() {
|
||||
const namedIcons = [];
|
||||
iconNames.forEach((name, index) => {
|
||||
if (name.trim()) {
|
||||
namedIcons.push({
|
||||
index: index + 1,
|
||||
name: name.trim(),
|
||||
url: icons[index]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const csvContent = 'Index,Name,URL\n' +
|
||||
namedIcons.map(icon => `${icon.index},"${icon.name}","${icon.url}"`).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'named_icons.csv';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Load icons when page loads
|
||||
window.onload = loadIcons;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
"id","created_at","updated_at","deleted_at","url","group_id","icon_name"
|
||||
"01KD7DGWH6TDB47V4710FDT7SR","2025-12-24 05:32:46.503+00","2025-12-24 05:32:46.503+00",NULL,"https://storage.danakcorp.com/images/1766554365832-411750172.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DH9F6BN4TEM96TA2QT4GJ","2025-12-24 05:32:59.751+00","2025-12-24 05:32:59.751+00",NULL,"https://storage.danakcorp.com/images/1766554379149-104970938.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DHK6ZTEGPRBNXJPV49TE6","2025-12-24 05:33:09.727+00","2025-12-24 05:33:09.727+00",NULL,"https://storage.danakcorp.com/images/1766554389090-593863597.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJ2NVEHVQMDCAM31VKKGK","2025-12-24 05:33:25.564+00","2025-12-24 05:33:25.564+00",NULL,"https://storage.danakcorp.com/images/1766554404968-905984091.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJCQY1V84DAVXPV097HE6","2025-12-24 05:33:35.871+00","2025-12-24 05:33:35.871+00",NULL,"https://storage.danakcorp.com/images/1766554415325-873800591.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DJSZ2XVX971FGNFRRDQV6","2025-12-24 05:33:49.411+00","2025-12-24 05:33:49.411+00",NULL,"https://storage.danakcorp.com/images/1766554428814-77027818.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DK3HJAD9SMDD8CM1RPNQS","2025-12-24 05:33:59.218+00","2025-12-24 05:33:59.218+00",NULL,"https://storage.danakcorp.com/images/1766554438632-45072052.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DKB23B6XWJ8CMAKQ5R8HS","2025-12-24 05:34:06.915+00","2025-12-24 05:34:06.915+00",NULL,"https://storage.danakcorp.com/images/1766554446340-463157109.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DKN32VCDQDF2D407MGKF0","2025-12-24 05:34:17.186+00","2025-12-24 05:34:17.186+00",NULL,"https://storage.danakcorp.com/images/1766554456605-280166176.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DM4AEQ2FCJGZM390N7YVJ","2025-12-24 05:34:32.782+00","2025-12-24 05:34:32.782+00",NULL,"https://storage.danakcorp.com/images/1766554471887-315640495.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DMHKWJSHX6BR41FTDYVHE","2025-12-24 05:34:46.396+00","2025-12-24 05:34:46.396+00",NULL,"https://storage.danakcorp.com/images/1766554485634-242317100.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DN3XNDW2JQ0RGM711R9Q5","2025-12-24 05:35:05.141+00","2025-12-24 05:35:05.141+00",NULL,"https://storage.danakcorp.com/images/1766554504553-157481346.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DNV0F9CPMKQ4R482MTVKJ","2025-12-24 05:35:28.783+00","2025-12-24 05:35:28.783+00",NULL,"https://storage.danakcorp.com/images/1766554528198-919877647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DPAKCPXD2GK0FCZ7N3XXP","2025-12-24 05:35:44.749+00","2025-12-24 05:35:44.749+00",NULL,"https://storage.danakcorp.com/images/1766554544140-886744157.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DPZGZFZPS5AJ8RNM3W16F","2025-12-24 05:36:06.175+00","2025-12-24 05:36:06.175+00",NULL,"https://storage.danakcorp.com/images/1766554565614-684869010.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DQCMAD39T5QB5728M6PFP","2025-12-24 05:36:19.594+00","2025-12-24 05:36:19.594+00",NULL,"https://storage.danakcorp.com/images/1766554578855-318583145.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DQTQHGWP5J77V327Y4GHZ","2025-12-24 05:36:34.034+00","2025-12-24 05:36:34.034+00",NULL,"https://storage.danakcorp.com/images/1766554593272-362758712.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DR75VSDY0B28AFTB6QP9M","2025-12-24 05:36:46.779+00","2025-12-24 05:36:46.779+00",NULL,"https://storage.danakcorp.com/images/1766554606166-877198688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DRRT3B7BAA714MMYVVHN4","2025-12-24 05:37:04.835+00","2025-12-24 05:37:04.835+00",NULL,"https://storage.danakcorp.com/images/1766554624133-21439996.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DS9TQY1KWPM2HY7VC7THR","2025-12-24 05:37:22.263+00","2025-12-24 05:37:22.263+00",NULL,"https://storage.danakcorp.com/images/1766554641691-535696200.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DST85ZZ0SKSVDYJWN67VQ","2025-12-24 05:37:39.078+00","2025-12-24 05:37:39.078+00",NULL,"https://storage.danakcorp.com/images/1766554658481-114730426.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7DT75Y84N1ECNV2WW6VQ37","2025-12-24 05:37:52.318+00","2025-12-24 05:37:52.318+00",NULL,"https://storage.danakcorp.com/images/1766554671760-634916570.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7E3837FJ9TDDKPF8S6PHK6","2025-12-24 05:42:48.167+00","2025-12-24 05:42:48.167+00",NULL,"https://storage.danakcorp.com/images/1766554967381-660612838.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7E7G89J5KH2ZT45F0MT5X3","2025-12-24 05:45:07.593+00","2025-12-24 05:45:07.593+00",NULL,"https://storage.danakcorp.com/images/1766555106925-267924006.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EBA6ATBBFVMGWQHACZ1G7","2025-12-24 05:47:12.459+00","2025-12-24 05:47:12.459+00",NULL,"https://storage.danakcorp.com/images/1766555231674-387319393.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ED3NRKEX3Z8G6CTHK5QAB","2025-12-24 05:48:11.32+00","2025-12-24 05:48:11.32+00",NULL,"https://storage.danakcorp.com/images/1766555290519-155637622.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EDN07W94SGG04BRNRNRWT","2025-12-24 05:48:29.063+00","2025-12-24 05:48:29.063+00",NULL,"https://storage.danakcorp.com/images/1766555308301-230359494.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EH2W75B2H8JGMHR9P76ZE","2025-12-24 05:50:21.576+00","2025-12-24 05:50:21.576+00",NULL,"https://storage.danakcorp.com/images/1766555420851-186440015.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EP8V3KQYMDCDMXYP1QKPK","2025-12-24 05:53:11.523+00","2025-12-24 05:53:11.523+00",NULL,"https://storage.danakcorp.com/images/1766555590828-644527730.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EPRJWQAQX00F0J02FV05J","2025-12-24 05:53:27.644+00","2025-12-24 05:53:27.644+00",NULL,"https://storage.danakcorp.com/images/1766555606997-571682462.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ERGH1ABTPGSJN1WFTESP6","2025-12-24 05:54:24.929+00","2025-12-24 05:54:24.929+00",NULL,"https://storage.danakcorp.com/images/1766555663659-433052581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ET0394XN31KFT3340R04H","2025-12-24 05:55:13.641+00","2025-12-24 05:55:13.641+00",NULL,"https://storage.danakcorp.com/images/1766555713098-988242030.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7ETCZBFV8ZZF1GFDKP7DXA","2025-12-24 05:55:26.827+00","2025-12-24 05:55:26.827+00",NULL,"https://storage.danakcorp.com/images/1766555726158-680948882.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EV0VW1M18RGBB54C4YDAC","2025-12-24 05:55:47.196+00","2025-12-24 05:55:47.196+00",NULL,"https://storage.danakcorp.com/images/1766555746650-426516324.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EVMP392QF71V6QFGK9XQA","2025-12-24 05:56:07.491+00","2025-12-24 05:56:07.491+00",NULL,"https://storage.danakcorp.com/images/1766555765798-127083646.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EW6MQX0VFPS21TJPSEBAH","2025-12-24 05:56:25.879+00","2025-12-24 05:56:25.879+00",NULL,"https://storage.danakcorp.com/images/1766555784837-243715698.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EWMGEHYQRKBM6XBS9EKTF","2025-12-24 05:56:40.078+00","2025-12-24 05:56:40.078+00",NULL,"https://storage.danakcorp.com/images/1766555799191-594035927.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7EZHSEJ4G27VT3HS7JZ8YN","2025-12-24 05:58:15.599+00","2025-12-24 05:58:15.599+00",NULL,"https://storage.danakcorp.com/images/1766555894624-524350777.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F0PCEADF1HWJ47V8RP1ZP","2025-12-24 05:58:53.07+00","2025-12-24 05:58:53.07+00",NULL,"https://storage.danakcorp.com/images/1766555932114-485954688.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F184CHB129A900K2V6N94","2025-12-24 05:59:11.244+00","2025-12-24 05:59:11.244+00",NULL,"https://storage.danakcorp.com/images/1766555949650-909996041.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F1R0JNNRP8QW8NHANZB01","2025-12-24 05:59:27.506+00","2025-12-24 05:59:27.506+00",NULL,"https://storage.danakcorp.com/images/1766555966861-75517122.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7F2Z1V0T1S47R7VWBEPY66","2025-12-24 06:00:07.483+00","2025-12-24 06:00:07.483+00",NULL,"https://storage.danakcorp.com/images/1766556006875-239689681.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FAP3S04D5CW51W98YZRHE","2025-12-24 06:04:20.473+00","2025-12-24 06:04:20.473+00",NULL,"https://storage.danakcorp.com/images/1766556257163-459958715.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FBQJAV9HJMG4EVRKPSAC5","2025-12-24 06:04:54.73+00","2025-12-24 06:04:54.73+00",NULL,"https://storage.danakcorp.com/images/1766556293812-665835248.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FC48FRARBP7ZXN7SPGP8K","2025-12-24 06:05:07.727+00","2025-12-24 06:05:07.727+00",NULL,"https://storage.danakcorp.com/images/1766556307082-162280793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FD4SW2G73QX0YY6KNK7V4","2025-12-24 06:05:41.052+00","2025-12-24 06:05:41.052+00",NULL,"https://storage.danakcorp.com/images/1766556340402-744632392.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FGRC85J6JXEYNBGCPY0S4","2025-12-24 06:07:39.4+00","2025-12-24 06:07:39.4+00",NULL,"https://storage.danakcorp.com/images/1766556458335-857124841.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FHCW86SPVBSC8N62YEY5R","2025-12-24 06:08:00.393+00","2025-12-24 06:08:00.393+00",NULL,"https://storage.danakcorp.com/images/1766556479530-161860621.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FHYWZ73T36XKTDQ5FEN7G","2025-12-24 06:08:18.847+00","2025-12-24 06:08:18.847+00",NULL,"https://storage.danakcorp.com/images/1766556498215-91175544.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FQDYPGWD05HHATWBC499Z","2025-12-24 06:11:18.103+00","2025-12-24 06:11:18.103+00",NULL,"https://storage.danakcorp.com/images/1766556677355-108854669.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FS7QGJJ3YRSM10Y5YWD6C","2025-12-24 06:12:17.264+00","2025-12-24 06:12:17.264+00",NULL,"https://storage.danakcorp.com/images/1766556736283-691587656.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FSY2XKBREKJ9HCNJN924C","2025-12-24 06:12:40.157+00","2025-12-24 06:12:40.157+00",NULL,"https://storage.danakcorp.com/images/1766556759367-58066921.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FTFZ25MHQATRC41Y3EPQS","2025-12-24 06:12:58.468+00","2025-12-24 06:12:58.468+00",NULL,"https://storage.danakcorp.com/images/1766556777353-490756125.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FXARH84SJYESXVAG0AH3Q","2025-12-24 06:14:31.441+00","2025-12-24 06:14:31.441+00",NULL,"https://storage.danakcorp.com/images/1766556870740-118553581.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FXXV4DP74NGWJC0J43MDP","2025-12-24 06:14:50.98+00","2025-12-24 06:14:50.98+00",NULL,"https://storage.danakcorp.com/images/1766556890334-962678132.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FYVRNQAAA0DQZ8NZ02F2R","2025-12-24 06:15:21.621+00","2025-12-24 06:15:21.621+00",NULL,"https://storage.danakcorp.com/images/1766556921029-865260745.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7FZMJ4QGTG6NZEDKWP7V0W","2025-12-24 06:15:47.013+00","2025-12-24 06:15:47.013+00",NULL,"https://storage.danakcorp.com/images/1766556946363-320833501.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G0F8GTY7KZ530Y9XF8YXF","2025-12-24 06:16:14.352+00","2025-12-24 06:16:14.352+00",NULL,"https://storage.danakcorp.com/images/1766556973771-832565827.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G300731YZQDT3FVAP0KHH","2025-12-24 06:17:37.031+00","2025-12-24 06:17:37.031+00",NULL,"https://storage.danakcorp.com/images/1766557056358-727862983.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G7BHM8WEXFHHKD09MG10J","2025-12-24 06:19:59.925+00","2025-12-24 06:19:59.925+00",NULL,"https://storage.danakcorp.com/images/1766557199259-72955190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G8ZM2FPZ9H5BVGD983CTS","2025-12-24 06:20:53.25+00","2025-12-24 06:20:53.25+00",NULL,"https://storage.danakcorp.com/images/1766557252452-988016525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7G9N7J647RH94K5WQB1Q3B","2025-12-24 06:21:15.378+00","2025-12-24 06:21:15.378+00",NULL,"https://storage.danakcorp.com/images/1766557274778-439028456.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GA36DDJBQJ15XT4D0E2D6","2025-12-24 06:21:29.678+00","2025-12-24 06:21:29.678+00",NULL,"https://storage.danakcorp.com/images/1766557289094-415104854.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GAEZKQKRC80EP7F03K22K","2025-12-24 06:21:41.747+00","2025-12-24 06:21:41.747+00",NULL,"https://storage.danakcorp.com/images/1766557300923-302277606.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GAYJAHXAZV84PN3X2N4FY","2025-12-24 06:21:57.707+00","2025-12-24 06:21:57.707+00",NULL,"https://storage.danakcorp.com/images/1766557317091-602116315.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GN1W5Q0WZWCJR8V62AD88","2025-12-24 06:27:28.774+00","2025-12-24 06:27:28.774+00",NULL,"https://storage.danakcorp.com/images/1766557647999-47333939.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GNPEX4CMZHRVYB4AZYSK3","2025-12-24 06:27:49.853+00","2025-12-24 06:27:49.853+00",NULL,"https://storage.danakcorp.com/images/1766557669282-172512141.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GPSY27QGH5XSPXWHD8VJ5","2025-12-24 06:28:26.179+00","2025-12-24 06:28:26.179+00",NULL,"https://storage.danakcorp.com/images/1766557705283-692546793.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GQGZK52YM87E283013JS4","2025-12-24 06:28:49.779+00","2025-12-24 06:28:49.779+00",NULL,"https://storage.danakcorp.com/images/1766557729175-762576743.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GRBRZTFFHDS1A6MH2CMJ4","2025-12-24 06:29:17.215+00","2025-12-24 06:29:17.215+00",NULL,"https://storage.danakcorp.com/images/1766557756487-665300947.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GRYXHE220H9C3VH4VAC8E","2025-12-24 06:29:36.817+00","2025-12-24 06:29:36.817+00",NULL,"https://storage.danakcorp.com/images/1766557776067-804149228.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GSKMZH8WAH5VYAS4GH02C","2025-12-24 06:29:58.048+00","2025-12-24 06:29:58.048+00",NULL,"https://storage.danakcorp.com/images/1766557797509-101726352.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GT6A83SX8NKPVHYXAKJ3H","2025-12-24 06:30:17.16+00","2025-12-24 06:30:17.16+00",NULL,"https://storage.danakcorp.com/images/1766557816484-357699340.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7GTTKT3H3EDFTRCY8P2AE9","2025-12-24 06:30:37.946+00","2025-12-24 06:30:37.946+00",NULL,"https://storage.danakcorp.com/images/1766557837292-326521286.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J0VP01FCP0TEHYG0MD191","2025-12-24 06:51:24.224+00","2025-12-24 06:51:24.224+00",NULL,"https://storage.danakcorp.com/images/1766559083575-564721059.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J1BMTMBMV009EYC7N4G7D","2025-12-24 06:51:40.57+00","2025-12-24 06:51:40.57+00",NULL,"https://storage.danakcorp.com/images/1766559099561-720966516.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7J1W90CASGWZKBFMN75STB","2025-12-24 06:51:57.601+00","2025-12-24 06:51:57.601+00",NULL,"https://storage.danakcorp.com/images/1766559116496-834964990.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NS1HJ2C707G3KXRRZCN7Y","2025-12-24 07:57:02.387+00","2025-12-24 07:57:02.387+00",NULL,"https://storage.danakcorp.com/images/1766563021426-600579018.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NSHYZ6XESEJR7MCKS7CY0","2025-12-24 07:57:19.199+00","2025-12-24 07:57:19.199+00",NULL,"https://storage.danakcorp.com/images/1766563038525-727531190.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NSYX5W066SDN0RYBKXHQ3","2025-12-24 07:57:32.454+00","2025-12-24 07:57:32.454+00",NULL,"https://storage.danakcorp.com/images/1766563051485-824602001.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NTCZK4NYE3KCYDM1247GP","2025-12-24 07:57:46.867+00","2025-12-24 07:57:46.867+00",NULL,"https://storage.danakcorp.com/images/1766563066017-372210647.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NW5KZEW4M2ZJX93T53G41","2025-12-24 07:58:44.863+00","2025-12-24 07:58:44.863+00",NULL,"https://storage.danakcorp.com/images/1766563124296-331607800.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NWQ7YFJ7HDNWRB5F1V47Q","2025-12-24 07:59:02.91+00","2025-12-24 07:59:02.91+00",NULL,"https://storage.danakcorp.com/images/1766563142133-359920423.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NXCZSEN7M2TN0DT0XVVR9","2025-12-24 07:59:25.177+00","2025-12-24 07:59:25.177+00",NULL,"https://storage.danakcorp.com/images/1766563164270-350101986.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NXXMYZFMAWMRS23GRZSKP","2025-12-24 07:59:42.238+00","2025-12-24 07:59:42.238+00",NULL,"https://storage.danakcorp.com/images/1766563181587-86630830.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NYP6M0HNC3A4XCXHK4W4M","2025-12-24 08:00:07.38+00","2025-12-24 08:00:07.38+00",NULL,"https://storage.danakcorp.com/images/1766563204966-651499380.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7NZ3PDZX4WTVV3KC5MTD8K","2025-12-24 08:00:21.198+00","2025-12-24 08:00:21.198+00",NULL,"https://storage.danakcorp.com/images/1766563220596-103699525.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P0FWPK3HTYVX14XMGCHTC","2025-12-24 08:01:06.455+00","2025-12-24 08:01:06.455+00",NULL,"https://storage.danakcorp.com/images/1766563265634-373953449.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P0SAN0PA6KAGE3ABAXSWF","2025-12-24 08:01:16.117+00","2025-12-24 08:01:16.117+00",NULL,"https://storage.danakcorp.com/images/1766563275517-958045207.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7P1429PQT8NFDFRZEDNH1P","2025-12-24 08:01:27.113+00","2025-12-24 08:01:27.113+00",NULL,"https://storage.danakcorp.com/images/1766563286526-622849562.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7QC4M3D2SF26ZH0S0SDWBD","2025-12-24 08:24:56.708+00","2025-12-24 08:24:56.708+00",NULL,"https://storage.danakcorp.com/images/1766564695339-504553420.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QCEDA0T3CGZ01NGBKWZ9S","2025-12-24 08:25:06.73+00","2025-12-24 08:25:06.73+00",NULL,"https://storage.danakcorp.com/images/1766564706032-159367016.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QCWQ00TVCK7BAAHSWA09H","2025-12-24 08:25:21.376+00","2025-12-24 08:25:21.376+00",NULL,"https://storage.danakcorp.com/images/1766564720683-887818252.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QDA1R9WTTV2JWQ2Y3WGST","2025-12-24 08:25:35.032+00","2025-12-24 08:25:35.032+00",NULL,"https://storage.danakcorp.com/images/1766564734432-483528219.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QDSCQNW2JC4SAQCK0QBNW","2025-12-24 08:25:50.743+00","2025-12-24 08:25:50.743+00",NULL,"https://storage.danakcorp.com/images/1766564750099-854146988.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QE1NXGG08JQ5F2BC35NFC","2025-12-24 08:25:59.23+00","2025-12-24 08:25:59.23+00",NULL,"https://storage.danakcorp.com/images/1766564758573-696060786.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QFFXDVTSB5VAXB640HR7N","2025-12-24 08:26:46.574+00","2025-12-24 08:26:46.574+00",NULL,"https://storage.danakcorp.com/images/1766564805823-829938380.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QJ37DT4NCD6QY9MNGCZXZ","2025-12-24 08:28:11.885+00","2025-12-24 08:28:11.885+00",NULL,"https://storage.danakcorp.com/images/1766564891137-468827500.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QJJZX6Y7CR8R60W3TNKRS","2025-12-24 08:28:28.029+00","2025-12-24 08:28:28.029+00",NULL,"https://storage.danakcorp.com/images/1766564907215-921380792.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7QK69ZH6B4S602YKZCBVMB","2025-12-24 08:28:47.807+00","2025-12-24 08:28:47.807+00",NULL,"https://storage.danakcorp.com/images/1766564926872-672612697.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R3EM739KW6PV431V6P19S","2025-12-24 08:37:40.616+00","2025-12-24 08:37:40.616+00",NULL,"https://storage.danakcorp.com/images/1766565459604-638026702.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R3YZ6SAQWZ4CMC4GCJ1V4","2025-12-24 08:37:57.351+00","2025-12-24 08:37:57.351+00",NULL,"https://storage.danakcorp.com/images/1766565476771-42527917.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R4D5BVGPWKXE6A7FNVNRA","2025-12-24 08:38:11.883+00","2025-12-24 08:38:11.883+00",NULL,"https://storage.danakcorp.com/images/1766565491264-946731491.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R4RFYFFQ4GV5TQ3DH6BW6","2025-12-24 08:38:23.486+00","2025-12-24 08:38:23.486+00",NULL,"https://storage.danakcorp.com/images/1766565502475-288189136.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R5KMSAGCXZCTQPTVYDTA7","2025-12-24 08:38:51.289+00","2025-12-24 08:38:51.289+00",NULL,"https://storage.danakcorp.com/images/1766565530628-800474580.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R64HYQD37W8ASB2Q817MW","2025-12-24 08:39:08.606+00","2025-12-24 08:39:08.606+00",NULL,"https://storage.danakcorp.com/images/1766565547983-878658938.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R6KF9YWZBZ3WPAKXQS2CH","2025-12-24 08:39:23.881+00","2025-12-24 08:39:23.881+00",NULL,"https://storage.danakcorp.com/images/1766565563263-891688321.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R7RM02R8FZ4DN65WMJVE8","2025-12-24 08:40:01.92+00","2025-12-24 08:40:01.92+00",NULL,"https://storage.danakcorp.com/images/1766565600547-704332792.svg","01KD7DG4VCTWZRSA54M3PEJ6NW"
|
||||
"01KD7R8G53XBC6AG6KAVSBW94G","2025-12-24 08:40:26.019+00","2025-12-24 08:40:26.019+00",NULL,"https://storage.danakcorp.com/images/1766565625437-528383713.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R901TPNAX9YYRG6W0GWHF","2025-12-24 08:40:42.298+00","2025-12-24 08:40:42.298+00",NULL,"https://storage.danakcorp.com/images/1766565641620-53176909.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7R9PKF9G1DD1HMS2QQQN9V","2025-12-24 08:41:05.391+00","2025-12-24 08:41:05.391+00",NULL,"https://storage.danakcorp.com/images/1766565664427-433669317.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RK2G6E5JKEDK9P95W79PR","2025-12-24 08:46:12.487+00","2025-12-24 08:46:12.487+00",NULL,"https://storage.danakcorp.com/images/1766565971515-159903323.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RKSM9QD6RA2H6WGE23KEC","2025-12-24 08:46:36.169+00","2025-12-24 08:46:36.169+00",NULL,"https://storage.danakcorp.com/images/1766565995424-288101561.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RMR9DRA1TG2BJ9Q8QAZ15","2025-12-24 08:47:07.565+00","2025-12-24 08:47:07.565+00",NULL,"https://storage.danakcorp.com/images/1766566026942-868807643.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RN6FH0TX69RZTKJKM6MEN","2025-12-24 08:47:22.097+00","2025-12-24 08:47:22.097+00",NULL,"https://storage.danakcorp.com/images/1766566041525-973019175.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RNX4EE92QR2C24577BJCW","2025-12-24 08:47:45.294+00","2025-12-24 08:47:45.294+00",NULL,"https://storage.danakcorp.com/images/1766566064741-271844433.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RPHHCKNXNQH29WSG84D2Z","2025-12-24 08:48:06.189+00","2025-12-24 08:48:06.189+00",NULL,"https://storage.danakcorp.com/images/1766566084368-438871956.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RQ6MFA5CDQ4J55NG3N7PX","2025-12-24 08:48:27.791+00","2025-12-24 08:48:27.791+00",NULL,"https://storage.danakcorp.com/images/1766566106954-47179607.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RQNXJNRXS8MXV57R50PKN","2025-12-24 08:48:43.443+00","2025-12-24 08:48:43.443+00",NULL,"https://storage.danakcorp.com/images/1766566122836-486871124.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RR9W6B9BTWJ0N2H11TWM1","2025-12-24 08:49:03.879+00","2025-12-24 08:49:03.879+00",NULL,"https://storage.danakcorp.com/images/1766566143171-668309076.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RRVQWYHSHN3M7R25GNGHS","2025-12-24 08:49:22.172+00","2025-12-24 08:49:22.172+00",NULL,"https://storage.danakcorp.com/images/1766566161551-844376214.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RSAKAP1TJ59GN11DEAXD9","2025-12-24 08:49:37.386+00","2025-12-24 08:49:37.386+00",NULL,"https://storage.danakcorp.com/images/1766566176755-30806596.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RSV35PY1A6193H4GDW1ZW","2025-12-24 08:49:54.277+00","2025-12-24 08:49:54.277+00",NULL,"https://storage.danakcorp.com/images/1766566193720-339416210.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RWX3D4ZZE35W345HDK35S","2025-12-24 08:51:34.638+00","2025-12-24 08:51:34.638+00",NULL,"https://storage.danakcorp.com/images/1766566293736-981836756.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RX79BVR30WGKXAZQ3D0CM","2025-12-24 08:51:45.068+00","2025-12-24 08:51:45.068+00",NULL,"https://storage.danakcorp.com/images/1766566304318-193301989.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RXY99C8PV6SAR0AJ00F16","2025-12-24 08:52:08.618+00","2025-12-24 08:52:08.618+00",NULL,"https://storage.danakcorp.com/images/1766566327768-2675508.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RYBPV273KCR350ESK2FN1","2025-12-24 08:52:22.364+00","2025-12-24 08:52:22.364+00",NULL,"https://storage.danakcorp.com/images/1766566341717-462491426.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RYTNTE20YS29X2XJT7GY3","2025-12-24 08:52:37.69+00","2025-12-24 08:52:37.69+00",NULL,"https://storage.danakcorp.com/images/1766566357028-432111036.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RZ8K86GCJSW1E0EC5T4B2","2025-12-24 08:52:51.944+00","2025-12-24 08:52:51.944+00",NULL,"https://storage.danakcorp.com/images/1766566371354-279596850.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7RZPNGASPYFP9XKMSY208B","2025-12-24 08:53:06.352+00","2025-12-24 08:53:06.352+00",NULL,"https://storage.danakcorp.com/images/1766566385674-569493499.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S0HEQZ4AY5TTFKF36HS6F","2025-12-24 08:53:33.783+00","2025-12-24 08:53:33.783+00",NULL,"https://storage.danakcorp.com/images/1766566413058-975642584.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S0ZEQ6BH163HCX0MQZHBJ","2025-12-24 08:53:48.119+00","2025-12-24 08:53:48.119+00",NULL,"https://storage.danakcorp.com/images/1766566427440-982841441.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S1EF6R6J6AMMC6HP5J0AA","2025-12-24 08:54:03.494+00","2025-12-24 08:54:03.494+00",NULL,"https://storage.danakcorp.com/images/1766566442808-761588425.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S27EFH2CDEK71GVQC839G","2025-12-24 08:54:29.074+00","2025-12-24 08:54:29.074+00",NULL,"https://storage.danakcorp.com/images/1766566468108-519151895.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S2N705Z754YS3YFQW4AGQ","2025-12-24 08:54:43.169+00","2025-12-24 08:54:43.169+00",NULL,"https://storage.danakcorp.com/images/1766566482278-661272876.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S31HNKPAAK58CX77BNJCA","2025-12-24 08:54:55.798+00","2025-12-24 08:54:55.798+00",NULL,"https://storage.danakcorp.com/images/1766566494050-408184442.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
"01KD7S3AX9VW5B830MYJYGXQ0B","2025-12-24 08:55:05.385+00","2025-12-24 08:55:05.385+00",NULL,"https://storage.danakcorp.com/images/1766566504760-709260995.svg","01KD7DGBDFX76VKXMKQKR51RJK"
|
||||
|
@@ -0,0 +1,205 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
\restrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
|
||||
|
||||
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
|
||||
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Data for Name: icon_groups; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DG4VCTWZRSA54M3PEJ6NW', '2025-12-24 05:32:22.252+00', '2025-12-24 05:32:22.252+00', NULL, 'پک آیکون 1');
|
||||
INSERT INTO public.icon_groups (id, created_at, updated_at, deleted_at, name) VALUES ('01KD7DGBDFX76VKXMKQKR51RJK', '2025-12-24 05:32:28.976+00', '2025-12-24 05:32:28.976+00', NULL, 'پک آیکون 2');
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
\unrestrict acoqslHA6I152rhJbwpzhi5E4Cp5r1Bq6kFOAPp82JWutcjW9v0rh7ptBQwHz6l
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
\restrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
|
||||
|
||||
-- Dumped from database version 17.6 (Debian 17.6-2.pgdg13+1)
|
||||
-- Dumped by pg_dump version 17.7 (Ubuntu 17.7-3.pgdg24.04+1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET transaction_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- Data for Name: icons; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DGWH6TDB47V4710FDT7SR', '2025-12-24 05:32:46.503+00', '2025-12-24 05:32:46.503+00', NULL, 'https://storage.danakcorp.com/images/1766554365832-411750172.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DH9F6BN4TEM96TA2QT4GJ', '2025-12-24 05:32:59.751+00', '2025-12-24 05:32:59.751+00', NULL, 'https://storage.danakcorp.com/images/1766554379149-104970938.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DHK6ZTEGPRBNXJPV49TE6', '2025-12-24 05:33:09.727+00', '2025-12-24 05:33:09.727+00', NULL, 'https://storage.danakcorp.com/images/1766554389090-593863597.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJ2NVEHVQMDCAM31VKKGK', '2025-12-24 05:33:25.564+00', '2025-12-24 05:33:25.564+00', NULL, 'https://storage.danakcorp.com/images/1766554404968-905984091.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJCQY1V84DAVXPV097HE6', '2025-12-24 05:33:35.871+00', '2025-12-24 05:33:35.871+00', NULL, 'https://storage.danakcorp.com/images/1766554415325-873800591.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DJSZ2XVX971FGNFRRDQV6', '2025-12-24 05:33:49.411+00', '2025-12-24 05:33:49.411+00', NULL, 'https://storage.danakcorp.com/images/1766554428814-77027818.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DK3HJAD9SMDD8CM1RPNQS', '2025-12-24 05:33:59.218+00', '2025-12-24 05:33:59.218+00', NULL, 'https://storage.danakcorp.com/images/1766554438632-45072052.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKB23B6XWJ8CMAKQ5R8HS', '2025-12-24 05:34:06.915+00', '2025-12-24 05:34:06.915+00', NULL, 'https://storage.danakcorp.com/images/1766554446340-463157109.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DKN32VCDQDF2D407MGKF0', '2025-12-24 05:34:17.186+00', '2025-12-24 05:34:17.186+00', NULL, 'https://storage.danakcorp.com/images/1766554456605-280166176.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DM4AEQ2FCJGZM390N7YVJ', '2025-12-24 05:34:32.782+00', '2025-12-24 05:34:32.782+00', NULL, 'https://storage.danakcorp.com/images/1766554471887-315640495.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DMHKWJSHX6BR41FTDYVHE', '2025-12-24 05:34:46.396+00', '2025-12-24 05:34:46.396+00', NULL, 'https://storage.danakcorp.com/images/1766554485634-242317100.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DN3XNDW2JQ0RGM711R9Q5', '2025-12-24 05:35:05.141+00', '2025-12-24 05:35:05.141+00', NULL, 'https://storage.danakcorp.com/images/1766554504553-157481346.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DNV0F9CPMKQ4R482MTVKJ', '2025-12-24 05:35:28.783+00', '2025-12-24 05:35:28.783+00', NULL, 'https://storage.danakcorp.com/images/1766554528198-919877647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPAKCPXD2GK0FCZ7N3XXP', '2025-12-24 05:35:44.749+00', '2025-12-24 05:35:44.749+00', NULL, 'https://storage.danakcorp.com/images/1766554544140-886744157.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DPZGZFZPS5AJ8RNM3W16F', '2025-12-24 05:36:06.175+00', '2025-12-24 05:36:06.175+00', NULL, 'https://storage.danakcorp.com/images/1766554565614-684869010.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQCMAD39T5QB5728M6PFP', '2025-12-24 05:36:19.594+00', '2025-12-24 05:36:19.594+00', NULL, 'https://storage.danakcorp.com/images/1766554578855-318583145.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DQTQHGWP5J77V327Y4GHZ', '2025-12-24 05:36:34.034+00', '2025-12-24 05:36:34.034+00', NULL, 'https://storage.danakcorp.com/images/1766554593272-362758712.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DR75VSDY0B28AFTB6QP9M', '2025-12-24 05:36:46.779+00', '2025-12-24 05:36:46.779+00', NULL, 'https://storage.danakcorp.com/images/1766554606166-877198688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DRRT3B7BAA714MMYVVHN4', '2025-12-24 05:37:04.835+00', '2025-12-24 05:37:04.835+00', NULL, 'https://storage.danakcorp.com/images/1766554624133-21439996.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DS9TQY1KWPM2HY7VC7THR', '2025-12-24 05:37:22.263+00', '2025-12-24 05:37:22.263+00', NULL, 'https://storage.danakcorp.com/images/1766554641691-535696200.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DST85ZZ0SKSVDYJWN67VQ', '2025-12-24 05:37:39.078+00', '2025-12-24 05:37:39.078+00', NULL, 'https://storage.danakcorp.com/images/1766554658481-114730426.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7DT75Y84N1ECNV2WW6VQ37', '2025-12-24 05:37:52.318+00', '2025-12-24 05:37:52.318+00', NULL, 'https://storage.danakcorp.com/images/1766554671760-634916570.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E3837FJ9TDDKPF8S6PHK6', '2025-12-24 05:42:48.167+00', '2025-12-24 05:42:48.167+00', NULL, 'https://storage.danakcorp.com/images/1766554967381-660612838.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7E7G89J5KH2ZT45F0MT5X3', '2025-12-24 05:45:07.593+00', '2025-12-24 05:45:07.593+00', NULL, 'https://storage.danakcorp.com/images/1766555106925-267924006.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EBA6ATBBFVMGWQHACZ1G7', '2025-12-24 05:47:12.459+00', '2025-12-24 05:47:12.459+00', NULL, 'https://storage.danakcorp.com/images/1766555231674-387319393.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ED3NRKEX3Z8G6CTHK5QAB', '2025-12-24 05:48:11.32+00', '2025-12-24 05:48:11.32+00', NULL, 'https://storage.danakcorp.com/images/1766555290519-155637622.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EDN07W94SGG04BRNRNRWT', '2025-12-24 05:48:29.063+00', '2025-12-24 05:48:29.063+00', NULL, 'https://storage.danakcorp.com/images/1766555308301-230359494.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EH2W75B2H8JGMHR9P76ZE', '2025-12-24 05:50:21.576+00', '2025-12-24 05:50:21.576+00', NULL, 'https://storage.danakcorp.com/images/1766555420851-186440015.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EP8V3KQYMDCDMXYP1QKPK', '2025-12-24 05:53:11.523+00', '2025-12-24 05:53:11.523+00', NULL, 'https://storage.danakcorp.com/images/1766555590828-644527730.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EPRJWQAQX00F0J02FV05J', '2025-12-24 05:53:27.644+00', '2025-12-24 05:53:27.644+00', NULL, 'https://storage.danakcorp.com/images/1766555606997-571682462.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ERGH1ABTPGSJN1WFTESP6', '2025-12-24 05:54:24.929+00', '2025-12-24 05:54:24.929+00', NULL, 'https://storage.danakcorp.com/images/1766555663659-433052581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ET0394XN31KFT3340R04H', '2025-12-24 05:55:13.641+00', '2025-12-24 05:55:13.641+00', NULL, 'https://storage.danakcorp.com/images/1766555713098-988242030.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7ETCZBFV8ZZF1GFDKP7DXA', '2025-12-24 05:55:26.827+00', '2025-12-24 05:55:26.827+00', NULL, 'https://storage.danakcorp.com/images/1766555726158-680948882.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EV0VW1M18RGBB54C4YDAC', '2025-12-24 05:55:47.196+00', '2025-12-24 05:55:47.196+00', NULL, 'https://storage.danakcorp.com/images/1766555746650-426516324.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EVMP392QF71V6QFGK9XQA', '2025-12-24 05:56:07.491+00', '2025-12-24 05:56:07.491+00', NULL, 'https://storage.danakcorp.com/images/1766555765798-127083646.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EW6MQX0VFPS21TJPSEBAH', '2025-12-24 05:56:25.879+00', '2025-12-24 05:56:25.879+00', NULL, 'https://storage.danakcorp.com/images/1766555784837-243715698.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EWMGEHYQRKBM6XBS9EKTF', '2025-12-24 05:56:40.078+00', '2025-12-24 05:56:40.078+00', NULL, 'https://storage.danakcorp.com/images/1766555799191-594035927.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7EZHSEJ4G27VT3HS7JZ8YN', '2025-12-24 05:58:15.599+00', '2025-12-24 05:58:15.599+00', NULL, 'https://storage.danakcorp.com/images/1766555894624-524350777.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F0PCEADF1HWJ47V8RP1ZP', '2025-12-24 05:58:53.07+00', '2025-12-24 05:58:53.07+00', NULL, 'https://storage.danakcorp.com/images/1766555932114-485954688.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F184CHB129A900K2V6N94', '2025-12-24 05:59:11.244+00', '2025-12-24 05:59:11.244+00', NULL, 'https://storage.danakcorp.com/images/1766555949650-909996041.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F1R0JNNRP8QW8NHANZB01', '2025-12-24 05:59:27.506+00', '2025-12-24 05:59:27.506+00', NULL, 'https://storage.danakcorp.com/images/1766555966861-75517122.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7F2Z1V0T1S47R7VWBEPY66', '2025-12-24 06:00:07.483+00', '2025-12-24 06:00:07.483+00', NULL, 'https://storage.danakcorp.com/images/1766556006875-239689681.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FAP3S04D5CW51W98YZRHE', '2025-12-24 06:04:20.473+00', '2025-12-24 06:04:20.473+00', NULL, 'https://storage.danakcorp.com/images/1766556257163-459958715.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FBQJAV9HJMG4EVRKPSAC5', '2025-12-24 06:04:54.73+00', '2025-12-24 06:04:54.73+00', NULL, 'https://storage.danakcorp.com/images/1766556293812-665835248.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FC48FRARBP7ZXN7SPGP8K', '2025-12-24 06:05:07.727+00', '2025-12-24 06:05:07.727+00', NULL, 'https://storage.danakcorp.com/images/1766556307082-162280793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FD4SW2G73QX0YY6KNK7V4', '2025-12-24 06:05:41.052+00', '2025-12-24 06:05:41.052+00', NULL, 'https://storage.danakcorp.com/images/1766556340402-744632392.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FGRC85J6JXEYNBGCPY0S4', '2025-12-24 06:07:39.4+00', '2025-12-24 06:07:39.4+00', NULL, 'https://storage.danakcorp.com/images/1766556458335-857124841.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHCW86SPVBSC8N62YEY5R', '2025-12-24 06:08:00.393+00', '2025-12-24 06:08:00.393+00', NULL, 'https://storage.danakcorp.com/images/1766556479530-161860621.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FHYWZ73T36XKTDQ5FEN7G', '2025-12-24 06:08:18.847+00', '2025-12-24 06:08:18.847+00', NULL, 'https://storage.danakcorp.com/images/1766556498215-91175544.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FQDYPGWD05HHATWBC499Z', '2025-12-24 06:11:18.103+00', '2025-12-24 06:11:18.103+00', NULL, 'https://storage.danakcorp.com/images/1766556677355-108854669.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FS7QGJJ3YRSM10Y5YWD6C', '2025-12-24 06:12:17.264+00', '2025-12-24 06:12:17.264+00', NULL, 'https://storage.danakcorp.com/images/1766556736283-691587656.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FSY2XKBREKJ9HCNJN924C', '2025-12-24 06:12:40.157+00', '2025-12-24 06:12:40.157+00', NULL, 'https://storage.danakcorp.com/images/1766556759367-58066921.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FTFZ25MHQATRC41Y3EPQS', '2025-12-24 06:12:58.468+00', '2025-12-24 06:12:58.468+00', NULL, 'https://storage.danakcorp.com/images/1766556777353-490756125.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXARH84SJYESXVAG0AH3Q', '2025-12-24 06:14:31.441+00', '2025-12-24 06:14:31.441+00', NULL, 'https://storage.danakcorp.com/images/1766556870740-118553581.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FXXV4DP74NGWJC0J43MDP', '2025-12-24 06:14:50.98+00', '2025-12-24 06:14:50.98+00', NULL, 'https://storage.danakcorp.com/images/1766556890334-962678132.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FYVRNQAAA0DQZ8NZ02F2R', '2025-12-24 06:15:21.621+00', '2025-12-24 06:15:21.621+00', NULL, 'https://storage.danakcorp.com/images/1766556921029-865260745.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7FZMJ4QGTG6NZEDKWP7V0W', '2025-12-24 06:15:47.013+00', '2025-12-24 06:15:47.013+00', NULL, 'https://storage.danakcorp.com/images/1766556946363-320833501.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G0F8GTY7KZ530Y9XF8YXF', '2025-12-24 06:16:14.352+00', '2025-12-24 06:16:14.352+00', NULL, 'https://storage.danakcorp.com/images/1766556973771-832565827.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G300731YZQDT3FVAP0KHH', '2025-12-24 06:17:37.031+00', '2025-12-24 06:17:37.031+00', NULL, 'https://storage.danakcorp.com/images/1766557056358-727862983.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G7BHM8WEXFHHKD09MG10J', '2025-12-24 06:19:59.925+00', '2025-12-24 06:19:59.925+00', NULL, 'https://storage.danakcorp.com/images/1766557199259-72955190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G8ZM2FPZ9H5BVGD983CTS', '2025-12-24 06:20:53.25+00', '2025-12-24 06:20:53.25+00', NULL, 'https://storage.danakcorp.com/images/1766557252452-988016525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7G9N7J647RH94K5WQB1Q3B', '2025-12-24 06:21:15.378+00', '2025-12-24 06:21:15.378+00', NULL, 'https://storage.danakcorp.com/images/1766557274778-439028456.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GA36DDJBQJ15XT4D0E2D6', '2025-12-24 06:21:29.678+00', '2025-12-24 06:21:29.678+00', NULL, 'https://storage.danakcorp.com/images/1766557289094-415104854.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAEZKQKRC80EP7F03K22K', '2025-12-24 06:21:41.747+00', '2025-12-24 06:21:41.747+00', NULL, 'https://storage.danakcorp.com/images/1766557300923-302277606.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GAYJAHXAZV84PN3X2N4FY', '2025-12-24 06:21:57.707+00', '2025-12-24 06:21:57.707+00', NULL, 'https://storage.danakcorp.com/images/1766557317091-602116315.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GN1W5Q0WZWCJR8V62AD88', '2025-12-24 06:27:28.774+00', '2025-12-24 06:27:28.774+00', NULL, 'https://storage.danakcorp.com/images/1766557647999-47333939.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GNPEX4CMZHRVYB4AZYSK3', '2025-12-24 06:27:49.853+00', '2025-12-24 06:27:49.853+00', NULL, 'https://storage.danakcorp.com/images/1766557669282-172512141.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GPSY27QGH5XSPXWHD8VJ5', '2025-12-24 06:28:26.179+00', '2025-12-24 06:28:26.179+00', NULL, 'https://storage.danakcorp.com/images/1766557705283-692546793.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GQGZK52YM87E283013JS4', '2025-12-24 06:28:49.779+00', '2025-12-24 06:28:49.779+00', NULL, 'https://storage.danakcorp.com/images/1766557729175-762576743.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRBRZTFFHDS1A6MH2CMJ4', '2025-12-24 06:29:17.215+00', '2025-12-24 06:29:17.215+00', NULL, 'https://storage.danakcorp.com/images/1766557756487-665300947.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GRYXHE220H9C3VH4VAC8E', '2025-12-24 06:29:36.817+00', '2025-12-24 06:29:36.817+00', NULL, 'https://storage.danakcorp.com/images/1766557776067-804149228.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GSKMZH8WAH5VYAS4GH02C', '2025-12-24 06:29:58.048+00', '2025-12-24 06:29:58.048+00', NULL, 'https://storage.danakcorp.com/images/1766557797509-101726352.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GT6A83SX8NKPVHYXAKJ3H', '2025-12-24 06:30:17.16+00', '2025-12-24 06:30:17.16+00', NULL, 'https://storage.danakcorp.com/images/1766557816484-357699340.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7GTTKT3H3EDFTRCY8P2AE9', '2025-12-24 06:30:37.946+00', '2025-12-24 06:30:37.946+00', NULL, 'https://storage.danakcorp.com/images/1766557837292-326521286.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J0VP01FCP0TEHYG0MD191', '2025-12-24 06:51:24.224+00', '2025-12-24 06:51:24.224+00', NULL, 'https://storage.danakcorp.com/images/1766559083575-564721059.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1BMTMBMV009EYC7N4G7D', '2025-12-24 06:51:40.57+00', '2025-12-24 06:51:40.57+00', NULL, 'https://storage.danakcorp.com/images/1766559099561-720966516.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7J1W90CASGWZKBFMN75STB', '2025-12-24 06:51:57.601+00', '2025-12-24 06:51:57.601+00', NULL, 'https://storage.danakcorp.com/images/1766559116496-834964990.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NS1HJ2C707G3KXRRZCN7Y', '2025-12-24 07:57:02.387+00', '2025-12-24 07:57:02.387+00', NULL, 'https://storage.danakcorp.com/images/1766563021426-600579018.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSHYZ6XESEJR7MCKS7CY0', '2025-12-24 07:57:19.199+00', '2025-12-24 07:57:19.199+00', NULL, 'https://storage.danakcorp.com/images/1766563038525-727531190.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NSYX5W066SDN0RYBKXHQ3', '2025-12-24 07:57:32.454+00', '2025-12-24 07:57:32.454+00', NULL, 'https://storage.danakcorp.com/images/1766563051485-824602001.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NTCZK4NYE3KCYDM1247GP', '2025-12-24 07:57:46.867+00', '2025-12-24 07:57:46.867+00', NULL, 'https://storage.danakcorp.com/images/1766563066017-372210647.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NW5KZEW4M2ZJX93T53G41', '2025-12-24 07:58:44.863+00', '2025-12-24 07:58:44.863+00', NULL, 'https://storage.danakcorp.com/images/1766563124296-331607800.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NWQ7YFJ7HDNWRB5F1V47Q', '2025-12-24 07:59:02.91+00', '2025-12-24 07:59:02.91+00', NULL, 'https://storage.danakcorp.com/images/1766563142133-359920423.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXCZSEN7M2TN0DT0XVVR9', '2025-12-24 07:59:25.177+00', '2025-12-24 07:59:25.177+00', NULL, 'https://storage.danakcorp.com/images/1766563164270-350101986.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NXXMYZFMAWMRS23GRZSKP', '2025-12-24 07:59:42.238+00', '2025-12-24 07:59:42.238+00', NULL, 'https://storage.danakcorp.com/images/1766563181587-86630830.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NYP6M0HNC3A4XCXHK4W4M', '2025-12-24 08:00:07.38+00', '2025-12-24 08:00:07.38+00', NULL, 'https://storage.danakcorp.com/images/1766563204966-651499380.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7NZ3PDZX4WTVV3KC5MTD8K', '2025-12-24 08:00:21.198+00', '2025-12-24 08:00:21.198+00', NULL, 'https://storage.danakcorp.com/images/1766563220596-103699525.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0FWPK3HTYVX14XMGCHTC', '2025-12-24 08:01:06.455+00', '2025-12-24 08:01:06.455+00', NULL, 'https://storage.danakcorp.com/images/1766563265634-373953449.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P0SAN0PA6KAGE3ABAXSWF', '2025-12-24 08:01:16.117+00', '2025-12-24 08:01:16.117+00', NULL, 'https://storage.danakcorp.com/images/1766563275517-958045207.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7P1429PQT8NFDFRZEDNH1P', '2025-12-24 08:01:27.113+00', '2025-12-24 08:01:27.113+00', NULL, 'https://storage.danakcorp.com/images/1766563286526-622849562.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QC4M3D2SF26ZH0S0SDWBD', '2025-12-24 08:24:56.708+00', '2025-12-24 08:24:56.708+00', NULL, 'https://storage.danakcorp.com/images/1766564695339-504553420.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCEDA0T3CGZ01NGBKWZ9S', '2025-12-24 08:25:06.73+00', '2025-12-24 08:25:06.73+00', NULL, 'https://storage.danakcorp.com/images/1766564706032-159367016.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QCWQ00TVCK7BAAHSWA09H', '2025-12-24 08:25:21.376+00', '2025-12-24 08:25:21.376+00', NULL, 'https://storage.danakcorp.com/images/1766564720683-887818252.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDA1R9WTTV2JWQ2Y3WGST', '2025-12-24 08:25:35.032+00', '2025-12-24 08:25:35.032+00', NULL, 'https://storage.danakcorp.com/images/1766564734432-483528219.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QDSCQNW2JC4SAQCK0QBNW', '2025-12-24 08:25:50.743+00', '2025-12-24 08:25:50.743+00', NULL, 'https://storage.danakcorp.com/images/1766564750099-854146988.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QE1NXGG08JQ5F2BC35NFC', '2025-12-24 08:25:59.23+00', '2025-12-24 08:25:59.23+00', NULL, 'https://storage.danakcorp.com/images/1766564758573-696060786.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QFFXDVTSB5VAXB640HR7N', '2025-12-24 08:26:46.574+00', '2025-12-24 08:26:46.574+00', NULL, 'https://storage.danakcorp.com/images/1766564805823-829938380.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJ37DT4NCD6QY9MNGCZXZ', '2025-12-24 08:28:11.885+00', '2025-12-24 08:28:11.885+00', NULL, 'https://storage.danakcorp.com/images/1766564891137-468827500.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QJJZX6Y7CR8R60W3TNKRS', '2025-12-24 08:28:28.029+00', '2025-12-24 08:28:28.029+00', NULL, 'https://storage.danakcorp.com/images/1766564907215-921380792.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7QK69ZH6B4S602YKZCBVMB', '2025-12-24 08:28:47.807+00', '2025-12-24 08:28:47.807+00', NULL, 'https://storage.danakcorp.com/images/1766564926872-672612697.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3EM739KW6PV431V6P19S', '2025-12-24 08:37:40.616+00', '2025-12-24 08:37:40.616+00', NULL, 'https://storage.danakcorp.com/images/1766565459604-638026702.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R3YZ6SAQWZ4CMC4GCJ1V4', '2025-12-24 08:37:57.351+00', '2025-12-24 08:37:57.351+00', NULL, 'https://storage.danakcorp.com/images/1766565476771-42527917.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4D5BVGPWKXE6A7FNVNRA', '2025-12-24 08:38:11.883+00', '2025-12-24 08:38:11.883+00', NULL, 'https://storage.danakcorp.com/images/1766565491264-946731491.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R4RFYFFQ4GV5TQ3DH6BW6', '2025-12-24 08:38:23.486+00', '2025-12-24 08:38:23.486+00', NULL, 'https://storage.danakcorp.com/images/1766565502475-288189136.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R5KMSAGCXZCTQPTVYDTA7', '2025-12-24 08:38:51.289+00', '2025-12-24 08:38:51.289+00', NULL, 'https://storage.danakcorp.com/images/1766565530628-800474580.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R64HYQD37W8ASB2Q817MW', '2025-12-24 08:39:08.606+00', '2025-12-24 08:39:08.606+00', NULL, 'https://storage.danakcorp.com/images/1766565547983-878658938.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R6KF9YWZBZ3WPAKXQS2CH', '2025-12-24 08:39:23.881+00', '2025-12-24 08:39:23.881+00', NULL, 'https://storage.danakcorp.com/images/1766565563263-891688321.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R7RM02R8FZ4DN65WMJVE8', '2025-12-24 08:40:01.92+00', '2025-12-24 08:40:01.92+00', NULL, 'https://storage.danakcorp.com/images/1766565600547-704332792.svg', '01KD7DG4VCTWZRSA54M3PEJ6NW');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R8G53XBC6AG6KAVSBW94G', '2025-12-24 08:40:26.019+00', '2025-12-24 08:40:26.019+00', NULL, 'https://storage.danakcorp.com/images/1766565625437-528383713.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R901TPNAX9YYRG6W0GWHF', '2025-12-24 08:40:42.298+00', '2025-12-24 08:40:42.298+00', NULL, 'https://storage.danakcorp.com/images/1766565641620-53176909.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7R9PKF9G1DD1HMS2QQQN9V', '2025-12-24 08:41:05.391+00', '2025-12-24 08:41:05.391+00', NULL, 'https://storage.danakcorp.com/images/1766565664427-433669317.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RK2G6E5JKEDK9P95W79PR', '2025-12-24 08:46:12.487+00', '2025-12-24 08:46:12.487+00', NULL, 'https://storage.danakcorp.com/images/1766565971515-159903323.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RKSM9QD6RA2H6WGE23KEC', '2025-12-24 08:46:36.169+00', '2025-12-24 08:46:36.169+00', NULL, 'https://storage.danakcorp.com/images/1766565995424-288101561.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RMR9DRA1TG2BJ9Q8QAZ15', '2025-12-24 08:47:07.565+00', '2025-12-24 08:47:07.565+00', NULL, 'https://storage.danakcorp.com/images/1766566026942-868807643.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RN6FH0TX69RZTKJKM6MEN', '2025-12-24 08:47:22.097+00', '2025-12-24 08:47:22.097+00', NULL, 'https://storage.danakcorp.com/images/1766566041525-973019175.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RNX4EE92QR2C24577BJCW', '2025-12-24 08:47:45.294+00', '2025-12-24 08:47:45.294+00', NULL, 'https://storage.danakcorp.com/images/1766566064741-271844433.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RPHHCKNXNQH29WSG84D2Z', '2025-12-24 08:48:06.189+00', '2025-12-24 08:48:06.189+00', NULL, 'https://storage.danakcorp.com/images/1766566084368-438871956.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQ6MFA5CDQ4J55NG3N7PX', '2025-12-24 08:48:27.791+00', '2025-12-24 08:48:27.791+00', NULL, 'https://storage.danakcorp.com/images/1766566106954-47179607.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RQNXJNRXS8MXV57R50PKN', '2025-12-24 08:48:43.443+00', '2025-12-24 08:48:43.443+00', NULL, 'https://storage.danakcorp.com/images/1766566122836-486871124.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RR9W6B9BTWJ0N2H11TWM1', '2025-12-24 08:49:03.879+00', '2025-12-24 08:49:03.879+00', NULL, 'https://storage.danakcorp.com/images/1766566143171-668309076.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RRVQWYHSHN3M7R25GNGHS', '2025-12-24 08:49:22.172+00', '2025-12-24 08:49:22.172+00', NULL, 'https://storage.danakcorp.com/images/1766566161551-844376214.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSAKAP1TJ59GN11DEAXD9', '2025-12-24 08:49:37.386+00', '2025-12-24 08:49:37.386+00', NULL, 'https://storage.danakcorp.com/images/1766566176755-30806596.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RSV35PY1A6193H4GDW1ZW', '2025-12-24 08:49:54.277+00', '2025-12-24 08:49:54.277+00', NULL, 'https://storage.danakcorp.com/images/1766566193720-339416210.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RWX3D4ZZE35W345HDK35S', '2025-12-24 08:51:34.638+00', '2025-12-24 08:51:34.638+00', NULL, 'https://storage.danakcorp.com/images/1766566293736-981836756.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RX79BVR30WGKXAZQ3D0CM', '2025-12-24 08:51:45.068+00', '2025-12-24 08:51:45.068+00', NULL, 'https://storage.danakcorp.com/images/1766566304318-193301989.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RXY99C8PV6SAR0AJ00F16', '2025-12-24 08:52:08.618+00', '2025-12-24 08:52:08.618+00', NULL, 'https://storage.danakcorp.com/images/1766566327768-2675508.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYBPV273KCR350ESK2FN1', '2025-12-24 08:52:22.364+00', '2025-12-24 08:52:22.364+00', NULL, 'https://storage.danakcorp.com/images/1766566341717-462491426.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RYTNTE20YS29X2XJT7GY3', '2025-12-24 08:52:37.69+00', '2025-12-24 08:52:37.69+00', NULL, 'https://storage.danakcorp.com/images/1766566357028-432111036.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZ8K86GCJSW1E0EC5T4B2', '2025-12-24 08:52:51.944+00', '2025-12-24 08:52:51.944+00', NULL, 'https://storage.danakcorp.com/images/1766566371354-279596850.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7RZPNGASPYFP9XKMSY208B', '2025-12-24 08:53:06.352+00', '2025-12-24 08:53:06.352+00', NULL, 'https://storage.danakcorp.com/images/1766566385674-569493499.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0HEQZ4AY5TTFKF36HS6F', '2025-12-24 08:53:33.783+00', '2025-12-24 08:53:33.783+00', NULL, 'https://storage.danakcorp.com/images/1766566413058-975642584.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S0ZEQ6BH163HCX0MQZHBJ', '2025-12-24 08:53:48.119+00', '2025-12-24 08:53:48.119+00', NULL, 'https://storage.danakcorp.com/images/1766566427440-982841441.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S1EF6R6J6AMMC6HP5J0AA', '2025-12-24 08:54:03.494+00', '2025-12-24 08:54:03.494+00', NULL, 'https://storage.danakcorp.com/images/1766566442808-761588425.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S27EFH2CDEK71GVQC839G', '2025-12-24 08:54:29.074+00', '2025-12-24 08:54:29.074+00', NULL, 'https://storage.danakcorp.com/images/1766566468108-519151895.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S2N705Z754YS3YFQW4AGQ', '2025-12-24 08:54:43.169+00', '2025-12-24 08:54:43.169+00', NULL, 'https://storage.danakcorp.com/images/1766566482278-661272876.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S31HNKPAAK58CX77BNJCA', '2025-12-24 08:54:55.798+00', '2025-12-24 08:54:55.798+00', NULL, 'https://storage.danakcorp.com/images/1766566494050-408184442.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
INSERT INTO public.icons (id, created_at, updated_at, deleted_at, url, group_id) VALUES ('01KD7S3AX9VW5B830MYJYGXQ0B', '2025-12-24 08:55:05.385+00', '2025-12-24 08:55:05.385+00', NULL, 'https://storage.danakcorp.com/images/1766566504760-709260995.svg', '01KD7DGBDFX76VKXMKQKR51RJK');
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
\unrestrict 4pvnNoptyhScdLjLBpsonU6ZnOH502ap2y2am0YHcRG7WkY1NxnW43VtUD3Gq85
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
Index,Name,URL
|
||||
1,bread,https://storage.danakcorp.com/images/1766554365832-411750172.svg
|
||||
2,avocado,https://storage.danakcorp.com/images/1766554379149-104970938.svg
|
||||
3,knife,https://storage.danakcorp.com/images/1766554389090-593863597.svg
|
||||
4,baby bottle,https://storage.danakcorp.com/images/1766554404968-905984091.svg
|
||||
5,baby bottle,https://storage.danakcorp.com/images/1766554415325-873800591.svg
|
||||
6,baby pacifier,https://storage.danakcorp.com/images/1766554428814-77027818.svg
|
||||
7,banana,https://storage.danakcorp.com/images/1766554438632-45072052.svg
|
||||
8,barbecue,https://storage.danakcorp.com/images/1766554446340-463157109.svg
|
||||
9,barbeque2,https://storage.danakcorp.com/images/1766554456605-280166176.svg
|
||||
10,drink,https://storage.danakcorp.com/images/1766554471887-315640495.svg
|
||||
11,hanburger,https://storage.danakcorp.com/images/1766554485634-242317100.svg
|
||||
12,drink2,https://storage.danakcorp.com/images/1766554504553-157481346.svg
|
||||
13,bottle1,https://storage.danakcorp.com/images/1766554528198-919877647.svg
|
||||
14,bottle2,https://storage.danakcorp.com/images/1766554544140-886744157.svg
|
||||
15,broccoli,https://storage.danakcorp.com/images/1766554565614-684869010.svg
|
||||
16,cake1,https://storage.danakcorp.com/images/1766554578855-318583145.svg
|
||||
17,cake2,https://storage.danakcorp.com/images/1766554593272-362758712.svg
|
||||
18,cake piece,https://storage.danakcorp.com/images/1766554606166-877198688.svg
|
||||
19,cup cake,https://storage.danakcorp.com/images/1766554624133-21439996.svg
|
||||
20,caupcake2,https://storage.danakcorp.com/images/1766554641691-535696200.svg
|
||||
21,candle,https://storage.danakcorp.com/images/1766554658481-114730426.svg
|
||||
22,candy,https://storage.danakcorp.com/images/1766554671760-634916570.svg
|
||||
23,carrot,https://storage.danakcorp.com/images/1766554967381-660612838.svg
|
||||
24,cheese,https://storage.danakcorp.com/images/1766555106925-267924006.svg
|
||||
25,cheese2,https://storage.danakcorp.com/images/1766555231674-387319393.svg
|
||||
26,hat robe,https://storage.danakcorp.com/images/1766555290519-155637622.svg
|
||||
27,hat robe 2,https://storage.danakcorp.com/images/1766555308301-230359494.svg
|
||||
28,cherry,https://storage.danakcorp.com/images/1766555420851-186440015.svg
|
||||
29,chicken,https://storage.danakcorp.com/images/1766555590828-644527730.svg
|
||||
30,baken,https://storage.danakcorp.com/images/1766555606997-571682462.svg
|
||||
31,lollipop,https://storage.danakcorp.com/images/1766555663659-433052581.svg
|
||||
32,cookie1,https://storage.danakcorp.com/images/1766555713098-988242030.svg
|
||||
33,cookie2,https://storage.danakcorp.com/images/1766555726158-680948882.svg
|
||||
34,crosson,https://storage.danakcorp.com/images/1766555746650-426516324.svg
|
||||
35,juice,https://storage.danakcorp.com/images/1766555765798-127083646.svg
|
||||
36,juice2,https://storage.danakcorp.com/images/1766555784837-243715698.svg
|
||||
37,juice3,https://storage.danakcorp.com/images/1766555799191-594035927.svg
|
||||
38,spoon and fork 1,https://storage.danakcorp.com/images/1766555894624-524350777.svg
|
||||
39,spoon and fork 2,https://storage.danakcorp.com/images/1766555932114-485954688.svg
|
||||
40,spoon and knife,https://storage.danakcorp.com/images/1766555949650-909996041.svg
|
||||
41,fork and knife,https://storage.danakcorp.com/images/1766555966861-75517122.svg
|
||||
42,hot dog,https://storage.danakcorp.com/images/1766556006875-239689681.svg
|
||||
43,soap,https://storage.danakcorp.com/images/1766556257163-459958715.svg
|
||||
44,dish,https://storage.danakcorp.com/images/1766556293812-665835248.svg
|
||||
45,donuts,https://storage.danakcorp.com/images/1766556307082-162280793.svg
|
||||
46,egg plant,https://storage.danakcorp.com/images/1766556340402-744632392.svg
|
||||
47,egg,https://storage.danakcorp.com/images/1766556458335-857124841.svg
|
||||
48,fast-food,https://storage.danakcorp.com/images/1766556479530-161860621.svg
|
||||
49,taco,https://storage.danakcorp.com/images/1766556498215-91175544.svg
|
||||
50,fire,https://storage.danakcorp.com/images/1766556677355-108854669.svg
|
||||
51,fish,https://storage.danakcorp.com/images/1766556736283-691587656.svg
|
||||
52,fish,https://storage.danakcorp.com/images/1766556759367-58066921.svg
|
||||
53,french fries,https://storage.danakcorp.com/images/1766556777353-490756125.svg
|
||||
54,fried egg,https://storage.danakcorp.com/images/1766556870740-118553581.svg
|
||||
55,refrigerator,https://storage.danakcorp.com/images/1766556890334-962678132.svg
|
||||
56,grape,https://storage.danakcorp.com/images/1766556921029-865260745.svg
|
||||
57,hat robe,https://storage.danakcorp.com/images/1766556946363-320833501.svg
|
||||
58,hot dog,https://storage.danakcorp.com/images/1766556973771-832565827.svg
|
||||
59,ice cream,https://storage.danakcorp.com/images/1766557056358-727862983.svg
|
||||
60,ice cream,https://storage.danakcorp.com/images/1766557199259-72955190.svg
|
||||
61,ice cream 3,https://storage.danakcorp.com/images/1766557252452-988016525.svg
|
||||
62,jar,https://storage.danakcorp.com/images/1766557274778-439028456.svg
|
||||
63,jelly,https://storage.danakcorp.com/images/1766557289094-415104854.svg
|
||||
64,juice1,https://storage.danakcorp.com/images/1766557300923-302277606.svg
|
||||
65,juice 2,https://storage.danakcorp.com/images/1766557317091-602116315.svg
|
||||
66,kitchen stove,https://storage.danakcorp.com/images/1766557647999-47333939.svg
|
||||
67,orange slash,https://storage.danakcorp.com/images/1766557669282-172512141.svg
|
||||
68,loaf of bread,https://storage.danakcorp.com/images/1766557705283-692546793.svg
|
||||
69,loaf of bread 2,https://storage.danakcorp.com/images/1766557729175-762576743.svg
|
||||
70,meat,https://storage.danakcorp.com/images/1766557756487-665300947.svg
|
||||
71,meat 2,https://storage.danakcorp.com/images/1766557776067-804149228.svg
|
||||
72,mixer,https://storage.danakcorp.com/images/1766557797509-101726352.svg
|
||||
73,mortar,https://storage.danakcorp.com/images/1766557816484-357699340.svg
|
||||
74,noodle,https://storage.danakcorp.com/images/1766557837292-326521286.svg
|
||||
75,nuts,https://storage.danakcorp.com/images/1766559083575-564721059.svg
|
||||
76,olive,https://storage.danakcorp.com/images/1766559099561-720966516.svg
|
||||
77,onion,https://storage.danakcorp.com/images/1766559116496-834964990.svg
|
||||
78,pizza,https://storage.danakcorp.com/images/1766563021426-600579018.svg
|
||||
79,rolling pin,https://storage.danakcorp.com/images/1766563038525-727531190.svg
|
||||
80,solt,https://storage.danakcorp.com/images/1766563051485-824602001.svg
|
||||
81,scales,https://storage.danakcorp.com/images/1766563066017-372210647.svg
|
||||
82,basket,https://storage.danakcorp.com/images/1766563124296-331607800.svg
|
||||
83,skewer,https://storage.danakcorp.com/images/1766563142133-359920423.svg
|
||||
84,strawberry,https://storage.danakcorp.com/images/1766563164270-350101986.svg
|
||||
85,toast,https://storage.danakcorp.com/images/1766563181587-86630830.svg
|
||||
86,watermelon,https://storage.danakcorp.com/images/1766563204966-651499380.svg
|
||||
87,whisk,https://storage.danakcorp.com/images/1766563220596-103699525.svg
|
||||
88,wine glass,https://storage.danakcorp.com/images/1766563265634-373953449.svg
|
||||
89,wine glass 2,https://storage.danakcorp.com/images/1766563275517-958045207.svg
|
||||
90,wine glass 3,https://storage.danakcorp.com/images/1766563286526-622849562.svg
|
||||
91,colored bakeries,https://storage.danakcorp.com/images/1766564695339-504553420.svg
|
||||
92,colored bio,https://storage.danakcorp.com/images/1766564706032-159367016.svg
|
||||
93,colored bibimbap,https://storage.danakcorp.com/images/1766564720683-887818252.svg
|
||||
94,colored boba,https://storage.danakcorp.com/images/1766564734432-483528219.svg
|
||||
95,colored breakfast,https://storage.danakcorp.com/images/1766564750099-854146988.svg
|
||||
96,colored hamburger,https://storage.danakcorp.com/images/1766564758573-696060786.svg
|
||||
97,colored hamburger 2,https://storage.danakcorp.com/images/1766564805823-829938380.svg
|
||||
98,colored coffee,https://storage.danakcorp.com/images/1766564891137-468827500.svg
|
||||
99,colored coca,https://storage.danakcorp.com/images/1766564907215-921380792.svg
|
||||
100,colored cookies,https://storage.danakcorp.com/images/1766564926872-672612697.svg
|
||||
101,colored noodle,https://storage.danakcorp.com/images/1766565459604-638026702.svg
|
||||
102,colored crossan,https://storage.danakcorp.com/images/1766565476771-42527917.svg
|
||||
103,colored sweet,https://storage.danakcorp.com/images/1766565491264-946731491.svg
|
||||
104,colored dumpling,https://storage.danakcorp.com/images/1766565502475-288189136.svg
|
||||
105,colored fish and chips,https://storage.danakcorp.com/images/1766565530628-800474580.svg
|
||||
106,colored focaccia,https://storage.danakcorp.com/images/1766565547983-878658938.svg
|
||||
107,colored french fries,https://storage.danakcorp.com/images/1766565563263-891688321.svg
|
||||
108,colored fried chicken,https://storage.danakcorp.com/images/1766565600547-704332792.svg
|
||||
109,colored fried rice,https://storage.danakcorp.com/images/1766565625437-528383713.svg
|
||||
110,colored fries,https://storage.danakcorp.com/images/1766565641620-53176909.svg
|
||||
111,hat robe,https://storage.danakcorp.com/images/1766565664427-433669317.svg
|
||||
112,colored hot dog,https://storage.danakcorp.com/images/1766565971515-159903323.svg
|
||||
113,colored hot dog,https://storage.danakcorp.com/images/1766565995424-288101561.svg
|
||||
114,colored hot pot,https://storage.danakcorp.com/images/1766566026942-868807643.svg
|
||||
115,colored ice cream,https://storage.danakcorp.com/images/1766566041525-973019175.svg
|
||||
116,colored ice tea,https://storage.danakcorp.com/images/1766566064741-271844433.svg
|
||||
117,colored instant noodle cup,https://storage.danakcorp.com/images/1766566084368-438871956.svg
|
||||
118,lemon,https://storage.danakcorp.com/images/1766566106954-47179607.svg
|
||||
119,colored martini,https://storage.danakcorp.com/images/1766566122836-486871124.svg
|
||||
120,colored match,https://storage.danakcorp.com/images/1766566143171-668309076.svg
|
||||
121,colored mochi donuts,https://storage.danakcorp.com/images/1766566161551-844376214.svg
|
||||
122,colored mochi,https://storage.danakcorp.com/images/1766566176755-30806596.svg
|
||||
123,colored nachos,https://storage.danakcorp.com/images/1766566193720-339416210.svg
|
||||
124,colored origini,https://storage.danakcorp.com/images/1766566293736-981836756.svg
|
||||
125,colored pan cake,https://storage.danakcorp.com/images/1766566304318-193301989.svg
|
||||
126,colored pepperoni pizza,https://storage.danakcorp.com/images/1766566327768-2675508.svg
|
||||
127,colored pizza 4,https://storage.danakcorp.com/images/1766566341717-462491426.svg
|
||||
128,colored podding,https://storage.danakcorp.com/images/1766566357028-432111036.svg
|
||||
129,quesadilla,https://storage.danakcorp.com/images/1766566371354-279596850.svg
|
||||
130,salad,https://storage.danakcorp.com/images/1766566385674-569493499.svg
|
||||
131,colored sandwich,https://storage.danakcorp.com/images/1766566413058-975642584.svg
|
||||
132,colored stak,https://storage.danakcorp.com/images/1766566427440-982841441.svg
|
||||
133,colored soshi,https://storage.danakcorp.com/images/1766566442808-761588425.svg
|
||||
134,colored taco,https://storage.danakcorp.com/images/1766566468108-519151895.svg
|
||||
135,colored tempura,https://storage.danakcorp.com/images/1766566482278-661272876.svg
|
||||
136,colored tafu,https://storage.danakcorp.com/images/1766566494050-408184442.svg
|
||||
137,whisk,https://storage.danakcorp.com/images/1766566504760-709260995.svg
|
||||
|
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read the CSV file
|
||||
const csvPath = path.join(__dirname, 'icons.csv');
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf8');
|
||||
|
||||
// Parse CSV lines
|
||||
const lines = csvContent.split('\n').filter(line => line.trim());
|
||||
const header = lines[0];
|
||||
const dataLines = lines.slice(1);
|
||||
|
||||
// Icon names - replace these with your actual icon names after reviewing them
|
||||
const iconNames = [
|
||||
// Add your icon names here, one for each row
|
||||
// Example: "hamburger", "pizza", "drink", etc.
|
||||
// For now, placeholders:
|
||||
"icon_1", "icon_2", "icon_3", "icon_4", "icon_5", "icon_6", "icon_7", "icon_8", "icon_9", "icon_10",
|
||||
"icon_11", "icon_12", "icon_13", "icon_14", "icon_15", "icon_16", "icon_17", "icon_18", "icon_19", "icon_20",
|
||||
"icon_21", "icon_22", "icon_23", "icon_24", "icon_25", "icon_26", "icon_27", "icon_28", "icon_29", "icon_30",
|
||||
"icon_31", "icon_32", "icon_33", "icon_34", "icon_35", "icon_36", "icon_37", "icon_38", "icon_39", "icon_40",
|
||||
"icon_41", "icon_42", "icon_43", "icon_44", "icon_45", "icon_46", "icon_47", "icon_48", "icon_49", "icon_50",
|
||||
"icon_51", "icon_52", "icon_53", "icon_54", "icon_55", "icon_56", "icon_57", "icon_58", "icon_59", "icon_60",
|
||||
"icon_61", "icon_62", "icon_63", "icon_64", "icon_65", "icon_66", "icon_67", "icon_68", "icon_69", "icon_70",
|
||||
"icon_71", "icon_72", "icon_73", "icon_74", "icon_75", "icon_76", "icon_77", "icon_78", "icon_79", "icon_80",
|
||||
"icon_81", "icon_82", "icon_83", "icon_84", "icon_85", "icon_86", "icon_87", "icon_88", "icon_89", "icon_90",
|
||||
"icon_91", "icon_92", "icon_93", "icon_94", "icon_95", "icon_96", "icon_97", "icon_98", "icon_99", "icon_100",
|
||||
"icon_101", "icon_102", "icon_103", "icon_104", "icon_105", "icon_106", "icon_107", "icon_108", "icon_109", "icon_110",
|
||||
"icon_111", "icon_112", "icon_113", "icon_114", "icon_115", "icon_116", "icon_117", "icon_118", "icon_119", "icon_120",
|
||||
"icon_121", "icon_122", "icon_123", "icon_124", "icon_125", "icon_126", "icon_127", "icon_128", "icon_129", "icon_130",
|
||||
"icon_131", "icon_132", "icon_133", "icon_134", "icon_135", "icon_136", "icon_137", "icon_138"
|
||||
];
|
||||
|
||||
// Validate that we have the right number of names
|
||||
if (iconNames.length !== dataLines.length) {
|
||||
console.error(`Error: You provided ${iconNames.length} icon names but there are ${dataLines.length} data rows in the CSV.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create new CSV content with icon names
|
||||
const newLines = [header];
|
||||
|
||||
dataLines.forEach((line, index) => {
|
||||
// Remove trailing comma if present and add the icon name
|
||||
const cleanLine = line.replace(/,$/, '');
|
||||
newLines.push(`${cleanLine},"${iconNames[index]}"`);
|
||||
});
|
||||
|
||||
// Write the updated CSV
|
||||
const newCsvContent = newLines.join('\n');
|
||||
fs.writeFileSync(csvPath, newCsvContent, 'utf8');
|
||||
|
||||
console.log(`✅ Successfully added ${iconNames.length} icon names to icons.csv`);
|
||||
console.log('📁 File saved:', csvPath);
|
||||
|
||||
// Show summary
|
||||
const namedCount = iconNames.filter(name => name && !name.startsWith('icon_')).length;
|
||||
console.log(`📊 ${namedCount} custom names, ${iconNames.length - namedCount} placeholder names`);
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to restore icon and icon_groups tables to PostgreSQL database
|
||||
# Usage: ./restore-icons.sh [dump_file]
|
||||
|
||||
set -e
|
||||
|
||||
# Load environment variables if .env file exists
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# Source .env file, handling comments and empty lines
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Remove leading/trailing whitespace
|
||||
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
# Skip empty lines and lines starting with #
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
# Remove inline comments (everything after # that's not in quotes)
|
||||
line=$(echo "$line" | sed 's/#.*$//' | sed 's/[[:space:]]*$//')
|
||||
# Skip if line is empty after removing comments
|
||||
[[ -z "$line" ]] && continue
|
||||
# Export the variable (only if it looks like KEY=VALUE)
|
||||
if [[ "$line" =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
|
||||
export "$line" 2>/dev/null || true
|
||||
fi
|
||||
done < .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Get database connection details from environment variables
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-dmenu}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
|
||||
# Input file name
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Please provide a dump file to restore"
|
||||
echo "Usage: ./restore-icons.sh <dump_file>"
|
||||
echo "Example: ./restore-icons.sh icons_dump_20240101_120000.sql"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DUMP_FILE="$1"
|
||||
|
||||
# Check if dump file exists
|
||||
if [ ! -f "${DUMP_FILE}" ]; then
|
||||
echo "Error: Dump file '${DUMP_FILE}' not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restoring icon_groups and icons tables..."
|
||||
echo "Database: ${DB_NAME}@${DB_HOST}:${DB_PORT}"
|
||||
echo "Dump file: ${DUMP_FILE}"
|
||||
echo ""
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "This will replace existing data in icon_groups and icons tables. Continue? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Restore cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Disable foreign key checks temporarily and truncate tables
|
||||
echo "Clearing existing data..."
|
||||
PGPASSWORD="${DB_PASS}" psql \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-c "TRUNCATE TABLE icons CASCADE;" \
|
||||
-c "TRUNCATE TABLE icon_groups CASCADE;"
|
||||
|
||||
# Restore data from dump file
|
||||
echo "Restoring data from ${DUMP_FILE}..."
|
||||
PGPASSWORD="${DB_PASS}" psql \
|
||||
-h "${DB_HOST}" \
|
||||
-p "${DB_PORT}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
-f "${DUMP_FILE}"
|
||||
|
||||
echo ""
|
||||
echo "Restore completed successfully!"
|
||||
echo ""
|
||||
|
||||
# Verify the restore
|
||||
echo "Verifying restore..."
|
||||
ICON_GROUPS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icon_groups;")
|
||||
ICONS_COUNT=$(PGPASSWORD="${DB_PASS}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -t -c "SELECT COUNT(*) FROM icons;")
|
||||
|
||||
echo "icon_groups records: ${ICON_GROUPS_COUNT}"
|
||||
echo "icons records: ${ICONS_COUNT}"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// @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',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
+17122
File diff suppressed because it is too large
Load Diff
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"name": "base-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts ",
|
||||
"db:drop": "npx mikro-orm schema:drop --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:seed": "npx mikro-orm seeder:run --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"db:reset": "npm run db:drop && npm run db:create && npm run db:seed",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start dist/main",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node --max-old-space-size=2048 dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"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",
|
||||
"migration:create": "npx mikro-orm migration:create --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:up": "npx mikro-orm migration:up --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:down": "npx mikro-orm migration:down --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:list": "npx mikro-orm migration:list --config ./src/config/mikro-orm.config.dev.ts",
|
||||
"migration:pending": "npx mikro-orm migration:pending --config ./src/config/mikro-orm.config.dev.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^5.1.0",
|
||||
"@as-integrations/fastify": "^3.1.0",
|
||||
"@aws-sdk/client-s3": "^3.922.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@keyv/redis": "^5.1.3",
|
||||
"@mikro-orm/core": "^6.5.8",
|
||||
"@mikro-orm/nestjs": "^6.1.1",
|
||||
"@mikro-orm/postgresql": "^6.5.8",
|
||||
"@nest-lab/fastify-multer": "^1.3.0",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.8",
|
||||
"@nestjs/platform-socket.io": "^11.1.9",
|
||||
"@nestjs/schedule": "^6.0.1",
|
||||
"@nestjs/swagger": "^11.2.1",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@types/socket.io": "^3.0.1",
|
||||
"axios": "^1.13.1",
|
||||
"bullmq": "^5.65.1",
|
||||
"cache-manager": "^7.2.4",
|
||||
"cacheable": "^2.3.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"fastify": "^5.6.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socket.io": "^4.8.1",
|
||||
"ulid": "^3.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@mikro-orm/cli": "^6.5.8",
|
||||
"@mikro-orm/migrations": "^6.5.8",
|
||||
"@mikro-orm/seeder": "^6.5.8",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Generated
+14002
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Module } from '@nestjs/common';
|
||||
import dataBaseConfig from './config/mikro-orm.config';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { UserModule } from './modules/users/user.module';
|
||||
import { UtilsModule } from './modules/utils/utils.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UploaderModule } from './modules/uploader/uploader.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { RestaurantsModule } from './modules/restaurants/restaurants.module';
|
||||
import { FoodModule } from './modules/foods/food.module';
|
||||
import { CartModule } from './modules/cart/cart.module';
|
||||
import { RolesModule } from './modules/roles/roles.module';
|
||||
import { PaymentsModule } from './modules/payments/payments.module';
|
||||
import { DeliveryModule } from './modules/delivery/delivery.module';
|
||||
import { OrdersModule } from './modules/orders/orders.module';
|
||||
import { CouponModule } from './modules/coupons/coupon.module';
|
||||
import { ReviewModule } from './modules/review/review.module';
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { PagerModule } from './modules/pager/pager.module';
|
||||
import { ContactModule } from './modules/contact/contact.module';
|
||||
import { InventoryModule } from './modules/inventory/inventory.module';
|
||||
import { IconsModule } from './modules/icons/icons.module';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { cacheConfig } from './config/cache.config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
||||
CacheModule.registerAsync(cacheConfig()),
|
||||
MikroOrmModule.forRootAsync(dataBaseConfig),
|
||||
UserModule,
|
||||
UtilsModule,
|
||||
AuthModule,
|
||||
UploaderModule,
|
||||
AdminModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60, // time window in seconds
|
||||
limit: 5, // max requests per window
|
||||
},
|
||||
]),
|
||||
ScheduleModule.forRoot(),
|
||||
RestaurantsModule,
|
||||
FoodModule,
|
||||
CartModule,
|
||||
RolesModule,
|
||||
PaymentsModule,
|
||||
DeliveryModule,
|
||||
OrdersModule,
|
||||
CouponModule,
|
||||
ReviewModule,
|
||||
NotificationsModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
PagerModule,
|
||||
ContactModule,
|
||||
InventoryModule,
|
||||
IconsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
// exports: [CacheService],
|
||||
})
|
||||
export class AppModule {}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||
export const AI_CONFIG = 'AI_CONFIG';
|
||||
export const MIKRO_ORM_QUERY_LOGGER = 'MIKRO_ORM_QUERY_LOGGER';
|
||||
export const NAJVA_CONFIG = 'NAJVA_CONFIG';
|
||||
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
||||
export const AUTH_THROTTLE_LIMIT = 5;
|
||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
||||
//
|
||||
export const CONSOLE_JWT_STRATEGY_NAME = 'console_jwt_strategy';
|
||||
export const LOCAL_JWT_STRATEGY_NAME = 'local_jwt_strategy';
|
||||
|
||||
export const API_HEADER_SLUG = {
|
||||
name: 'X-Slug',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
default: 'zhivan',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract userId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/profile')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getProfile(@UserId() userId: string) {
|
||||
* return this.userService.findById(userId);
|
||||
* }
|
||||
*/
|
||||
export const AdminId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { adminId?: string }>();
|
||||
return request.adminId || '';
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export { UserId } from './user-id.decorator';
|
||||
export { RestId } from './rest-id.decorator';
|
||||
export { RateLimit } from './rate-limit.decorator';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const PERMISSIONS_KEY = 'permissions';
|
||||
|
||||
export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { UseGuards, applyDecorators } from '@nestjs/common';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
|
||||
import {
|
||||
AUTH_THROTTLE_LIMIT,
|
||||
AUTH_THROTTLE_TTL,
|
||||
AUTH__REFRESH_THROTTLE_LIMIT,
|
||||
AUTH__REFRESH_THROTTLE_TTL,
|
||||
} from '../constants';
|
||||
|
||||
export const RateLimit = (limit: number, ttl: number) =>
|
||||
applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard));
|
||||
|
||||
// Predefined rate limits for common scenarios
|
||||
export const StrictRateLimit = () => RateLimit(AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL); // 5 requests per minute
|
||||
export const StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute
|
||||
export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails per minute
|
||||
export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract restId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.restId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/restaurants')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getRestaurants(@RestId() restId: string) {
|
||||
* return this.restaurantService.findById(restId);
|
||||
* }
|
||||
*/
|
||||
export const RestId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { restId?: string }>();
|
||||
return request.restId || '';
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
export const RestSlug = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { slug?: string }>();
|
||||
return request.slug || '';
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Decorator to extract userId from the authenticated request.
|
||||
* Must be used after AdminAuthGuard or AuthGuard that sets request.userId.
|
||||
*
|
||||
* @example
|
||||
* @Get('/profile')
|
||||
* @UseGuards(AdminAuthGuard)
|
||||
* getProfile(@UserId() userId: string) {
|
||||
* return this.userService.findById(userId);
|
||||
* }
|
||||
*/
|
||||
export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext): string => {
|
||||
const request = ctx.switchToHttp().getRequest<Request & { userId?: string }>();
|
||||
return request.userId || '';
|
||||
});
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { Index, PrimaryKey, Property, Filter, OptionalProps } from '@mikro-orm/core';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
|
||||
@Index({ properties: ['deletedAt'] })
|
||||
@Index({ properties: ['createdAt'] })
|
||||
export abstract class BaseEntity {
|
||||
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt' | 'deletedAt';
|
||||
|
||||
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
|
||||
id: string = ulid();
|
||||
|
||||
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||
createdAt: Date = new Date();
|
||||
|
||||
@Property({
|
||||
onUpdate: () => new Date(),
|
||||
defaultRaw: 'now()',
|
||||
columnType: 'timestamptz',
|
||||
})
|
||||
updatedAt: Date = new Date();
|
||||
|
||||
@Property({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
Executable
+781
@@ -0,0 +1,781 @@
|
||||
export const enum AuthMessage {
|
||||
UNAUTHORIZED_ACCESS = 'شما دسترسی لازم برای این عملیات را ندارید.',
|
||||
PHONE_REGISTERED = 'شماره ثبت شد',
|
||||
INVALID_PHONE_FORMAT = 'فرمت شماره تلفن صحیح نیست',
|
||||
OTP_SENT = 'کد یکبار مصرف به شماره شما ارسال شد.',
|
||||
INVALID_PHONE = 'شماره موبایل اشتباه می باشد',
|
||||
INVALID_CREDENTIAL = 'شماره موبایل یا رمز عبور اشتباه می باشد',
|
||||
OTP_FAILED = 'کد یا شماره موبایل اشتباه است',
|
||||
LOGIN_SUCCESS = 'ورود با موفقیت انجام شد',
|
||||
PASSWORD_LOGIN_SUCCESS = 'با موفقیت وارد شدید',
|
||||
FORGOT_PASSWORD_SUCCESS = 'درخواست فراموشی رمز عبور با موفقیت ثبت شد',
|
||||
PASSWORD_SET_SUCCESS = 'پسورد با موفقیت تنظیم شد',
|
||||
PASSWORD_UPDATE_SUCCESS = 'رمز عبور شما تغییر کرد',
|
||||
INVALID_OTP = 'کد صحیح نمیباشد یا منقضی شده است',
|
||||
PASSWORD_MISMATCH = 'رمز عبور یکسان نیست',
|
||||
INVALID_PASSWORD = 'ایمیل یا رمز عبور اشتباه است',
|
||||
INVALID_REPEAT_PASSWORD = 'تکرار رمز عبور اشتباه است',
|
||||
EMAIL_NOT_EMPTY = 'ایمیل نمیتواند خالی باشد.',
|
||||
INVALID_EMAIL_FORMAT = 'فرمت ایمیل صحیح نیست',
|
||||
PasswordNotEmpty = 'پسورد نمیتواند خالی باشد.',
|
||||
USER_NOT_FOUND = 'کاربری با این شماره وجود ندارد',
|
||||
ADMIN_NOT_FOUND = 'ادمینی با این شماره وجود ندارد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
INVALID_USER = 'کاربری با این مشخصات یافت نشد',
|
||||
PHONE_NOT_FOUND = 'کاربری با این شماره یافت نشد',
|
||||
TOKEN_EXPIRED = 'توکن منقضی شده است',
|
||||
TOKEN_INVALID = 'توکن نامعتبر است',
|
||||
BANNED = 'در حال حاضر شما امکان دسترسی به این سرویس را ندارید',
|
||||
PHONE_EXISTS = 'شماره تلفن قبلا ثبت شده است',
|
||||
ADMIN_CREATED = 'ادمین با موفقیت ایجاد شد',
|
||||
PERM_NOT_FOUND = 'دسترسی یافت نشد',
|
||||
INVALID_PASS_FORMAT = 'فرمت رمز عبور صحیح نیست',
|
||||
PHONE_NOT_EMPTY = 'شماره تلفن نمیتواند خالی باشد',
|
||||
PASSWORD_FORMAT_INVALID = 'رمز عبور باید به صورت رشته باشد',
|
||||
OTP_FORMAT_INVALID = 'کد یکبار مصرف باید عددی و ۵ رقمی باشد',
|
||||
PHONE_FORMAT_INVALID = 'شماره تلفن باید معتبر و به فرمت ایران باشد',
|
||||
TOKEN_NOT_EMPTY = 'توکن نمیتواند خالی باشد',
|
||||
PASSWORD_NOT_EMPTY = 'رمز عبور نمیتواند خالی باشد',
|
||||
PASSWORD_CHANGED_SUCCESSFULLY = 'رمز عبور با موفقیت تغییر کرد',
|
||||
PASSWORD_CONFIRMATION_NOT_EMPTY = 'تایید رمز عبور نمیتواند خالی باشد',
|
||||
PASSWORD_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
|
||||
OTP_NOT_EMPTY = 'کد یکبار مصرف نمیتواند خالی باشد',
|
||||
LAST_PASSWORD_NOT_EMPTY = 'رمز عبور قبلی نمیتواند خالی باشد',
|
||||
FIRST_NAME_NOT_EMPTY = 'نام نمیتواند خالی باشد',
|
||||
LAST_NAME_NOT_EMPTY = 'نام خانوادگی نمیتواند خالی باشد',
|
||||
FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام باید بین ۲ تا ۵۰ کاراکتر باشد',
|
||||
LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = 'نام خانوادگی باید بین ۲ تا ۵۰ کاراکتر باشد',
|
||||
BIRTH_DATE_NOT_EMPTY = 'تاریخ تولد نمیتواند خالی باشد',
|
||||
NATIONAL_CODE_INCORRECT = 'کد ملی باید ۱۰ رقمی باشد',
|
||||
NATIONAL_NOT_EMPTY = 'کد ملی نمیتواند خالی باشد',
|
||||
OTP_ALREADY_SENT = 'کد یکبار مصرف قبلا ارسال شده است',
|
||||
TOO_MANY_REQUESTS = 'تعداد درخواست های شما بیش از حد مجاز است',
|
||||
NOT_ADMIN = 'شما دسترسی به بخش ادمین ندارید',
|
||||
PHONE_SHOULD_BE_11_DIGIT = 'شماره تلفن باید ۱۱ رقم باشد',
|
||||
TIMESTAMP_NOT_EMPTY = 'زمان نمیتواند خالی باشد',
|
||||
ADMIN_CAN_NOT_LOGIN = 'ادمین نمیتواند وارد شود',
|
||||
NATIONAL_CODE_INVALID = 'کد ملی نامعتبر است',
|
||||
INVALID_REFRESH_TOKEN = 'توکن رفرش نامعتبر است',
|
||||
REFRESH_TOKEN_EXPIRED = 'توکن رفرش منقضی شده است',
|
||||
LOGOUT_SUCCESS = 'خروج با موفقیت انجام شد',
|
||||
CURRENT_PASSWORD_INVALID = 'رمز عبور فعلی نامعتبر است',
|
||||
TOKEN_EXPIRED_OR_INVALID = 'توکن منقضی شده یا نامعتبر است',
|
||||
ACCESS_DENIED = 'دسترسی شما محدود شده است',
|
||||
USER_ACCOUNT_INACTIVE = 'حساب کاربری شما غیر فعال است',
|
||||
TOKEN_MISSED_OR_EXPIRED = 'توکن مفقود یا منقضی شده است',
|
||||
OLD_PASSWORD_REQUIRED = 'رمز عبور قبلی الزامی است',
|
||||
PASSWORD_CHANGE_FAILED = 'تغییر رمز عبور با مشکل روبرد شد',
|
||||
SLUG_REQUIRED = 'اسلاگ الزامی است',
|
||||
INVALID_TOKEN_PAYLOAD = 'محتویات توکن نامعتبر است',
|
||||
INVALID_SLUG = 'اسلاگ نامعتبر است',
|
||||
INVALID_OR_EXPIRED_TOKEN = 'توکن نامعتبر یا منقضی شده است',
|
||||
}
|
||||
export const enum FoodMessage {
|
||||
NOT_FOUND = 'غذایی با این مشخصات یافت نشد',
|
||||
}
|
||||
export const enum CategoryMessage {
|
||||
NOT_FOUND = 'دستهبندی مورد نظر یافت نشد',
|
||||
NOT_CREATED = 'ایجاد دستهبندی با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی دستهبندی با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف دستهبندی با خطا مواجه شد',
|
||||
}
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = 'کاربری با این مشخصات یافت نشد',
|
||||
Rest_NOT_FOUND = 'رستورانی با این مشخصات یافت نشد',
|
||||
USER_EXISTS = 'با این شماره قبلا ثبت نام شده است',
|
||||
USER_REGISTER_SUCCESS = 'ثبت نام موفقیت آمیز بود',
|
||||
EMAIL_ADDRESS_ALREADY_EXISTS = 'ایمیل قبلا ثبت شده است',
|
||||
EMAIL_USER_NOT_FOUND = 'ایمیل یافت نشد',
|
||||
EMAIL_USER_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
|
||||
FAILED_TO_DELETE_EMAIL_USER = 'خطا در حذف ایمیل',
|
||||
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = 'ایمیل با موفقیت به روز رسانی شد',
|
||||
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = 'خطا در به روز رسانی حجم ایمیل',
|
||||
USERNAME_REQUIRED = 'نام کاربری الزامی است',
|
||||
USERNAME_STRING = 'نام کاربری باید یک رشته باشد',
|
||||
USERNAME_MIN_LENGTH = 'نام کاربری باید حداقل ۲ کاراکتر باشد',
|
||||
USERNAME_MATCHES = 'نام کاربری فقط میتواند شامل حروف، اعداد، نقطه، خط زیر و خط فاصله باشد',
|
||||
PASSWORD_MATCHES = 'رمز عبور باید حداقل ۸ کاراکتر باشد و شامل حروف بزرگ، حروف کوچک، اعداد و علائم خاص باشد',
|
||||
PASSWORD_STRING = 'رمز عبور باید یک رشته باشد',
|
||||
PASSWORD_REQUIRED = 'رمز عبور الزامی است',
|
||||
PASSWORD_MIN_LENGTH = 'رمز عبور باید حداقل ۸ کاراکتر باشد',
|
||||
DOMAIN_ID_STRING = 'شناسه دامنه باید یک رشته باشد',
|
||||
DOMAIN_ID_REQUIRED = 'شناسه دامنه الزامی است',
|
||||
DOMAIN_ID_UUID = 'شناسه دامنه باید یک یو یو آی دی باشد',
|
||||
DISPLAY_NAME_STRING = 'نام نمایشی باید یک رشته باشد',
|
||||
DISPLAY_NAME_OPTIONAL = 'نام نمایشی اختیاری است',
|
||||
QUOTA_NUMBER = 'حجم ایمیل باید یک عدد باشد',
|
||||
QUOTA_OPTIONAL = 'حجم ایمیل اختیاری است',
|
||||
QUOTA_NOT_EMPTY = 'حجم ایمیل نمیتواند خالی باشد',
|
||||
ALIASES_ARRAY = 'الیاس ها باید یک آرایه باشد',
|
||||
ALIASES_STRING = 'الیاس ها باید یک رشته باشد',
|
||||
ALIASES_OPTIONAL = 'الیاس ها اختیاری است',
|
||||
TITLE_STRING = 'عنوان باید یک رشته باشد',
|
||||
TITLE_OPTIONAL = 'عنوان اختیاری است',
|
||||
TITLE_NOT_EMPTY = 'عنوان نمیتواند خالی باشد',
|
||||
EMAIL_USER_CREATED_SUCCESSFULLY = 'ایمیل با موفقیت ایجاد شد',
|
||||
EMAIL_USER_UPDATED_SUCCESSFULLY = 'ایمیل کاربر با موفقیت بهروزرسانی شد',
|
||||
STATUS_UPDATED = 'وضعیت کاربر با موفقیت تغییر کرد',
|
||||
TEMPLATE_UUID = 'شناسه قالب باید یک یو یو آی دی باشد',
|
||||
TEMPLATE_NOT_EMPTY = 'شناسه قالب نمیتواند خالی باشد',
|
||||
ALIASES_EMAIL = 'آدرس ایمیل الیاس باید معتبر باشد',
|
||||
FORWARDERS_EMAIL_VALID = 'آدرس ایمیل فوروارد باید معتبر باشد',
|
||||
FORWARDERS_ARRAY = 'آدرس های فوروارد باید یک آرایه باشد',
|
||||
FORWARDERS_STRING = 'آدرس های فوروارد باید یک رشته باشد',
|
||||
PROFILE_PICTURE_URL = 'آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد',
|
||||
USER_UPDATED_SUCCESSFULLY = 'کاربر با موفقیت به روز رسانی شد',
|
||||
PROFILE_PICTURE_NOT_EMPTY = 'تصویر پروفایل نمیتواند خالی باشد',
|
||||
PUSH_TOKEN_NOT_FOUND = 'توکن پوش نامعتبر است',
|
||||
PUSH_TOKEN_ADDED_SUCCESSFULLY = 'توکن پوش با موفقیت اضافه شد',
|
||||
NO_PUSH_TOKENS_FOUND = 'توکن پوشی برای این کاربر یافت نشد',
|
||||
PUSH_TOKEN_REMOVED_SUCCESSFULLY = 'توکن پوش با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum CommonMessage {
|
||||
VALIDITY_TYPE_REQUIRED = 'نوع اعتبار سنجی مورد نیاز است',
|
||||
THIS_FILED_IS_REQUIRED = 'این فیلد الزامی است',
|
||||
VALID_FOR_CHOOSE = 'معتبر برای انتخاب',
|
||||
UPDATE_SUCCESS = 'با موفقیت به روز رسانی شد',
|
||||
CREATED = 'با موفقیت ایجاد شد',
|
||||
DELETED = 'با موفقیت حذف شد',
|
||||
ID_REQUIRED = 'شناسه مورد نیاز است',
|
||||
ID_SHOULD_BE_UUID = 'شناسه باید یک یو یو آی دی باشد',
|
||||
PaymentTypeQueryNotEmpty = 'نوع پرداخت نمیتواند خالی باشد',
|
||||
SEARCH_QUERY_STRING = 'رشته جستجو باید یک رشته باشد',
|
||||
UPDATED = 'با موفقیت آپدیت شد',
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = 'وضعیت باید یکی از مقادیر ۰ و ۱ باشد',
|
||||
DATE_MUST_BE_DATE = 'تاریخ باید یک تاریخ معتبر به صورت رشته باشد',
|
||||
}
|
||||
|
||||
export const enum UploaderMessage {
|
||||
UPLOAD_FILE_INVALID = 'فایل معتبر نیست',
|
||||
UPLOAD_FILE_TOO_LARGE = 'فایل بزرگتر از حد مجاز است . حداکثر حجم فایل [MAX_FILE_SIZE] مگابایت است',
|
||||
UPLOAD_FILE_TYPE_NOT_SUPPORTED = 'نوع فایل معتبر نیست . مجاز است : [MIME_TYPES]',
|
||||
UPLOAD_FILE_SUCCESS = 'فایل با موفقیت آپلود شد',
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
EMAIL_VERIFICATION = 'تایید ایمیل',
|
||||
EMAIL_SENDING_FAILED = 'ارسال ایمیل با خطا مواجه شد',
|
||||
LOGIN = 'ورود به سیستم',
|
||||
INVOICE = 'فاکتور',
|
||||
WALLET = 'اعلان کیف پول',
|
||||
TICKET = 'تیکت پشتیبانی',
|
||||
ANNOUNCEMENT = 'اعلان',
|
||||
PAYMENT_REMINDER = 'یادآوری پرداخت',
|
||||
PAYMENT_CANCELLATION = 'لغو پرداخت',
|
||||
EMAIL_SENDING_SUCCESS = 'ارسال ایمیل با موفقیت انجام شد',
|
||||
EMAIL_FORWARD_SUCCESS = 'فوروارد ایمیل با موفقیت انجام شد',
|
||||
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = 'وضعیت ایمیل با موفقیت دریافت شد',
|
||||
MESSAGES_SEARCHED_SUCCESSFULLY = 'جستجوی ایمیل با موفقیت انجام شد',
|
||||
MESSAGES_LISTED_SUCCESSFULLY = 'لیست ایمیل ها با موفقیت دریافت شد',
|
||||
MESSAGE_RETRIEVED_SUCCESSFULLY = 'ایمیل با موفقیت دریافت شد',
|
||||
MESSAGE_DELETED_SUCCESSFULLY = 'ایمیل با موفقیت حذف شد',
|
||||
USER_ID_REQUIRED = 'شناسه کاربری مورد نیاز است',
|
||||
USER_ID_MUST_BE_STRING = 'شناسه کاربری باید یک رشته باشد',
|
||||
MAILBOX_ID_REQUIRED = 'شناسه میل باکس ایمیل مورد نیاز است',
|
||||
MAILBOX_ID_MUST_BE_STRING = 'شناسه میل باکس ایمیل باید یک رشته باشد',
|
||||
QUEUE_ID_REQUIRED = 'شناسه پیشنویس ایمیل مورد نیاز است',
|
||||
QUEUE_ID_MUST_BE_STRING = 'شناسه پیشنویس ایمیل باید یک رشته باشد',
|
||||
MESSAGE_ID_MUST_BE_NUMBER = 'شناسه ایمیل باید یک عدد باشد',
|
||||
FROM_ADDRESS_REQUIRED = 'آدرس از مورد نیاز است',
|
||||
|
||||
// Validation Messages for Email DTOs
|
||||
FROM_ADDRESS_MUST_BE_EMAIL = 'آدرس فرستنده باید یک ایمیل معتبر باشد',
|
||||
|
||||
// Query validation messages
|
||||
SEARCH_QUERY_PARAMETER_MUST_BE_STRING = 'پارامتر جستجو باید یک رشته باشد',
|
||||
MESSAGE_IDS_MUST_BE_STRING = 'شناسههای پیام باید یک رشته باشد',
|
||||
THREAD_ID_MUST_BE_STRING = 'شناسه رشته گفتگو باید یک رشته باشد',
|
||||
OR_QUERY_MUST_BE_OBJECT = 'پارامتر OR باید یک شیء باشد',
|
||||
DATE_START_MUST_BE_DATE_STRING = 'تاریخ شروع باید یک رشته تاریخ معتبر باشد',
|
||||
DATE_END_MUST_BE_DATE_STRING = 'تاریخ پایان باید یک رشته تاریخ معتبر باشد',
|
||||
TO_ADDRESS_MUST_BE_STRING = 'آدرس گیرنده باید یک رشته باشد',
|
||||
MIN_SIZE_MUST_BE_NUMBER = 'حداقل اندازه باید یک عدد باشد',
|
||||
MIN_SIZE_MUST_BE_NON_NEGATIVE = 'حداقل اندازه نمیتواند منفی باشد',
|
||||
MAX_SIZE_MUST_BE_NUMBER = 'حداکثر اندازه باید یک عدد باشد',
|
||||
MAX_SIZE_MUST_BE_NON_NEGATIVE = 'حداکثر اندازه نمیتواند منفی باشد',
|
||||
ATTACHMENTS_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیوستها باید بولین باشد',
|
||||
FLAGGED_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیامهای علامتدار باید بولین باشد',
|
||||
UNSEEN_FILTER_MUST_BE_BOOLEAN = 'فیلتر پیامهای خواندهنشده باید بولین باشد',
|
||||
INCLUDE_HEADERS_MUST_BE_STRING = 'لیست هدرها باید یک رشته باشد',
|
||||
SEARCHABLE_FILTER_MUST_BE_BOOLEAN = 'فیلتر قابل جستجو باید بولین باشد',
|
||||
THREAD_COUNTERS_MUST_BE_BOOLEAN = 'شمارنده رشته گفتگو باید بولین باشد',
|
||||
ORDER_MUST_BE_VALID = 'ترتیب باید یکی از مقادیر asc یا desc باشد',
|
||||
NEXT_CURSOR_MUST_BE_STRING = 'نشانگر صفحه بعد باید یک رشته باشد',
|
||||
PREVIOUS_CURSOR_MUST_BE_STRING = 'نشانگر صفحه قبل باید یک رشته باشد',
|
||||
|
||||
RECIPIENT_NAME_MUST_BE_STRING = 'نام گیرنده باید یک رشته باشد',
|
||||
RECIPIENT_ADDRESS_REQUIRED = 'آدرس گیرنده مورد نیاز است',
|
||||
RECIPIENT_ADDRESS_MUST_BE_EMAIL = 'آدرس گیرنده باید یک ایمیل معتبر باشد',
|
||||
HEADER_KEY_REQUIRED = 'کلید هدر مورد نیاز است',
|
||||
HEADER_KEY_MUST_BE_STRING = 'کلید هدر باید یک رشته باشد',
|
||||
HEADER_VALUE_REQUIRED = 'مقدار هدر مورد نیاز است',
|
||||
HEADER_VALUE_MUST_BE_STRING = 'مقدار هدر باید یک رشته باشد',
|
||||
ATTACHMENT_FILENAME_MUST_BE_STRING = 'نام فایل پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING = 'نوع محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_ENCODING_MUST_BE_STRING = 'کدگذاری پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING = 'کدگذاری انتقال محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CONTENT_DISPOSITION_INVALID = 'نحوه نمایش پیوست باید inline یا attachment باشد',
|
||||
ATTACHMENT_CONTENT_REQUIRED = 'محتوای پیوست مورد نیاز است',
|
||||
ATTACHMENT_CONTENT_MUST_BE_STRING = 'محتوای پیوست باید یک رشته باشد',
|
||||
ATTACHMENT_CID_MUST_BE_STRING = 'شناسه محتوای پیوست باید یک رشته باشد',
|
||||
DRAFT_MAILBOX_REQUIRED = 'شناسه صندوق پیشنویس مورد نیاز است',
|
||||
DRAFT_MAILBOX_MUST_BE_STRING = 'شناسه صندوق پیشنویس باید یک رشته باشد',
|
||||
DRAFT_ID_REQUIRED = 'شناسه پیشنویس مورد نیاز است',
|
||||
DRAFT_ID_MUST_BE_NUMBER = 'شناسه پیشنویس باید یک عدد باشد',
|
||||
ENVELOPE_FROM_REQUIRED = 'آدرس فرستنده در envelope مورد نیاز است',
|
||||
ENVELOPE_TO_REQUIRED = 'آدرس گیرنده در envelope مورد نیاز است',
|
||||
ENVELOPE_TO_MUST_BE_ARRAY = 'آدرس گیرنده در envelope باید یک آرایه باشد',
|
||||
MAILBOX_MUST_BE_STRING = 'شناسه صندوق باید یک رشته باشد',
|
||||
REPLY_TO_MUST_BE_VALID = 'آدرس پاسخ باید معتبر باشد',
|
||||
TO_ADDRESSES_REQUIRED = 'آدرسهای گیرنده مورد نیاز است',
|
||||
TO_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای گیرنده باید یک آرایه باشد',
|
||||
CC_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای کپی کربن باید یک آرایه باشد',
|
||||
BCC_ADDRESSES_MUST_BE_ARRAY = 'آدرسهای کپی مخفی باید یک آرایه باشد',
|
||||
HEADERS_MUST_BE_ARRAY = 'هدرهای سفارشی باید یک آرایه باشد',
|
||||
SUBJECT_MUST_BE_STRING = 'موضوع ایمیل باید یک رشته باشد',
|
||||
TEXT_CONTENT_MUST_BE_STRING = 'محتوای متنی باید یک رشته باشد',
|
||||
HTML_CONTENT_MUST_BE_STRING = 'محتوای HTML باید یک رشته باشد',
|
||||
ATTACHMENTS_MUST_BE_ARRAY = 'پیوستها باید یک آرایه باشد',
|
||||
SESSION_ID_MUST_BE_STRING = 'شناسه جلسه باید یک رشته باشد',
|
||||
IP_ADDRESS_MUST_BE_STRING = 'آدرس IP باید یک رشته باشد',
|
||||
IS_DRAFT_MUST_BE_BOOLEAN = 'وضعیت پیشنویس باید بولین باشد',
|
||||
SEND_TIME_MUST_BE_DATE = 'زمان ارسال باید یک تاریخ معتبر باشد',
|
||||
UPLOAD_ONLY_MUST_BE_BOOLEAN = 'حالت فقط آپلود باید بولین باشد',
|
||||
ENVELOPE_MUST_BE_VALID = 'envelope باید معتبر باشد',
|
||||
|
||||
// Message operations
|
||||
MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده شده علامت گذاری شد',
|
||||
ALL_MESSAGES_MARKED_AS_READ_SUCCESSFULLY = 'تمام پیام ها با موفقیت به عنوان خوانده شده علامت گذاری شدند',
|
||||
MESSAGE_MARK_AS_SEEN_FAILED = 'علامت گذاری پیام با خطا مواجه شد',
|
||||
SENT_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های ارسالی با موفقیت دریافت شد',
|
||||
SENT_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های ارسالی با خطا مواجه شد',
|
||||
TRASH_MESSAGES_RETRIEVED_SUCCESSFULLY = 'پیام های حذف شده با موفقیت دریافت شد',
|
||||
TRASH_MESSAGES_RETRIEVAL_FAILED = 'دریافت پیام های حذف شده با خطا مواجه شد',
|
||||
DRAFT_UPDATED_SUCCESSFULLY = 'پیش نویس با موفقیت به روز رسانی شد',
|
||||
DRAFT_SENT_SUCCESSFULLY = 'پیش نویس با موفقیت ارسال شد',
|
||||
DRAFT_DELETED_SUCCESSFULLY = 'پیش نویس با موفقیت حذف شد',
|
||||
MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت به آرشیو منتقل شد',
|
||||
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به علاقه مندی ها منتقل شد',
|
||||
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = 'پیام با موفقیت به سطل زباله منتقل شد',
|
||||
MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = 'پیام با موفقیت به جانک منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = 'پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = 'پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد',
|
||||
MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY = 'پیام با موفقیت از جانک به صندوق ورودی منتقل شد',
|
||||
MESSAGE_NOT_FOUND = 'پیام یافت نشد',
|
||||
|
||||
// Bulk action messages
|
||||
BULK_ACTION_COMPLETED_SUCCESSFULLY = '[count] پیام با موفقیت [action] شد',
|
||||
BULK_ACTION_PARTIALLY_COMPLETED = '[successful] از [total] پیام [action] شد. [failed] پیام با خطا مواجه شد',
|
||||
BULK_ACTION_FAILED = 'هیچ پیامی [action] نشد. تمام [total] تلاش ناموفق بود',
|
||||
|
||||
// Bulk action display names
|
||||
BULK_ACTION_SEEN = 'خوانده شده علامتگذاری',
|
||||
BULK_ACTION_DELETE = 'حذف',
|
||||
BULK_ACTION_ARCHIVE = 'آرشیو',
|
||||
BULK_ACTION_UNARCHIVE = 'از آرشیو خارج',
|
||||
BULK_ACTION_FAVORITE = 'به علاقه مندی ها منتقل',
|
||||
BULK_ACTION_UNFAVORITE = 'از علاقه مندی ها حذف',
|
||||
BULK_ACTION_JUNK = 'به جانک منتقل',
|
||||
BULK_ACTION_NOTJUNK = 'از جانک از حذف',
|
||||
BULK_ACTION_TRASH = 'به سطل زباله منتقل',
|
||||
BULK_ACTION_RESTORE = 'از سطل زباله بازیابی',
|
||||
BULK_ACTION_PROCESSED = 'پردازش',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = 'اولویت باید یکی از مقادیر high, normal, low باشد',
|
||||
SUBJECT_REQUIRED = 'موضوع ایمیل مورد نیاز است',
|
||||
ALL_MESSAGES_DELETED_SUCCESSFULLY = 'همه پیام ها با موفقیت حذف شدند',
|
||||
MAILBOX_ID_MUST_BE_MONGO_ID = 'شناسه میل باکس ایمیل باید یک آی دی معتبر باشد',
|
||||
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = 'پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد',
|
||||
MESSAGE_MARK_AS_UNSEEN_FAILED = 'علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد',
|
||||
ATTACHMENT_ID_MUST_BE_STRING = 'شناسه پیوست باید یک رشته باشد',
|
||||
IS_FORWARD_MUST_BE_BOOLEAN = 'IS_FORWARD_MUST_BE_BOOLEAN',
|
||||
|
||||
// Favorite/Star messages (Gmail-like behavior)
|
||||
MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت به عنوان مورد علاقه علامت گذاری شد',
|
||||
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = 'پیام با موفقیت از مورد علاقه حذف شد',
|
||||
TRASH_EMPTYED_SUCCESSFULLY = 'سطل زباله با موفقیت خالی شد',
|
||||
SPAM_EMPTYED_SUCCESSFULLY = 'همه پیام های اسپم با موفقیت حذف شدند',
|
||||
SAVE_DRAFT_SUCCESS = 'پیش نویس با موفقیت ذخیره شد',
|
||||
HTML_CONTENT_REQUIRED = 'محتوای HTML مورد نیاز است',
|
||||
TEXT_CONTENT_REQUIRED = 'TEXT_CONTENT_REQUIRED',
|
||||
}
|
||||
|
||||
export const enum WebSocketMessage {
|
||||
// Connection Messages
|
||||
WS_CONNECTION_ESTABLISHED = 'اتصال وب سوکت با موفقیت برقرار شد',
|
||||
WS_CONNECTION_FAILED = 'اتصال وب سوکت ناموفق بود',
|
||||
WS_AUTHENTICATION_REQUIRED = 'احراز هویت وب سوکت مورد نیاز است',
|
||||
WS_AUTHENTICATION_FAILED = 'احراز هویت وب سوکت ناموفق بود',
|
||||
WS_AUTHENTICATION_SUCCESS = 'احراز هویت وب سوکت با موفقیت انجام شد',
|
||||
WS_INVALID_TOKEN = 'توکن وب سوکت نامعتبر است',
|
||||
WS_TOKEN_EXPIRED = 'توکن وب سوکت منقضی شده است',
|
||||
|
||||
// Room Management Messages
|
||||
JOINED_ROOM_SUCCESS = 'با موفقیت به اتاق متصل شدید',
|
||||
LEFT_ROOM_SUCCESS = 'با موفقیت از اتاق خارج شدید',
|
||||
ROOM_NOT_FOUND = 'اتاق یافت نشد',
|
||||
UNAUTHORIZED_ROOM_ACCESS = 'دسترسی غیرمجاز به اتاق',
|
||||
|
||||
// Session Management Messages
|
||||
WS_SESSION_CREATED = 'جلسه وب سوکت ایجاد شد',
|
||||
WS_SESSION_JOINED = 'به جلسه وب سوکت پیوستید',
|
||||
WS_SESSION_LEFT = 'از جلسه وب سوکت خارج شدید',
|
||||
WS_SESSION_EXPIRED = 'جلسه وب سوکت منقضی شده است',
|
||||
WS_SESSION_NOT_FOUND = 'جلسه وب سوکت یافت نشد',
|
||||
|
||||
// Error Messages
|
||||
WS_CONNECTION_ERROR = 'خطا در اتصال وب سوکت',
|
||||
INVALID_EVENT = 'رویداد نامعتبر',
|
||||
INVALID_DATA = 'داده نامعتبر',
|
||||
WS_RATE_LIMIT_EXCEEDED = 'تعداد درخواستهای وب سوکت از حد مجاز بیشتر است',
|
||||
INTERNAL_ERROR = 'خطای داخلی سرور',
|
||||
|
||||
// Notification Messages
|
||||
NEW_MESSAGE_NOTIFICATION = 'پیام جدید دریافت شد',
|
||||
MESSAGE_STATUS_UPDATED = 'وضعیت پیام بهروزرسانی شد',
|
||||
MESSAGE_MOVED_NOTIFICATION = 'پیام جابجا شد',
|
||||
MESSAGE_DELETED_NOTIFICATION = 'پیام حذف شد',
|
||||
|
||||
// General Messages
|
||||
OPERATION_SUCCESS = 'عملیات با موفقیت انجام شد',
|
||||
OPERATION_FAILED = 'عملیات ناموفق بود',
|
||||
PERMISSION_DENIED = 'مجوز دسترسی ندارید',
|
||||
RESOURCE_NOT_FOUND = 'منبع یافت نشد',
|
||||
|
||||
// Authentication errors
|
||||
AUTHENTICATION_REQUIRED = 'احراز هویت ضروری است',
|
||||
INVALID_TOKEN = 'توکن احراز هویت نامعتبر است',
|
||||
TOKEN_EXPIRED = 'توکن منقضی شده است',
|
||||
UNAUTHORIZED_ACCESS = 'دسترسی غیرمجاز',
|
||||
USER_NOT_AUTHENTICATED = 'کاربر احراز هویت نشده است',
|
||||
|
||||
// Connection errors
|
||||
CONNECTION_FAILED = 'اتصال برقرار نشد',
|
||||
WEBSOCKET_ERROR = 'خطا در ارتباط WebSocket',
|
||||
CONNECTION_TIMEOUT = 'زمان اتصال به پایان رسید',
|
||||
CONNECTION_REFUSED = 'اتصال رد شد',
|
||||
|
||||
// Session errors
|
||||
SESSION_NOT_FOUND = 'جلسه چت یافت نشد',
|
||||
SESSION_CREATION_FAILED = 'ایجاد جلسه چت با شکست مواجه شد',
|
||||
SESSION_ACCESS_DENIED = 'دسترسی به جلسه چت مجاز نیست',
|
||||
SESSION_EXPIRED = 'جلسه چت منقضی شده است',
|
||||
SESSION_ALREADY_EXISTS = 'جلسه چت قبلاً وجود دارد',
|
||||
|
||||
// Message errors
|
||||
MESSAGE_TOO_LONG = 'پیام از حداکثر طول مجاز تجاوز کرده است',
|
||||
MESSAGE_EMPTY = 'پیام نمیتواند خالی باشد',
|
||||
MESSAGE_SEND_FAILED = 'ارسال پیام با شکست مواجه شد',
|
||||
INVALID_MESSAGE_FORMAT = 'فرمت پیام نامعتبر است',
|
||||
|
||||
// Rate limiting
|
||||
RATE_LIMIT_EXCEEDED = 'تعداد پیامهای ارسالی بیش از حد مجاز است',
|
||||
TOO_MANY_CONNECTIONS = 'تعداد اتصالات بیش از حد مجاز است',
|
||||
|
||||
// Service errors
|
||||
LLM_SERVICE_ERROR = 'سرویس هوش مصنوعی موقتاً در دسترس نیست',
|
||||
SERVICE_UNAVAILABLE = 'سرویس موقتاً در دسترس نیست',
|
||||
INTERNAL_SERVER_ERROR = '.خطای داخلی سرور',
|
||||
|
||||
// Validation errors
|
||||
INVALID_SESSION_ID = 'شناسه جلسه نامعتبر است',
|
||||
INVALID_USER_ID = 'شناسه کاربر نامعتبر است',
|
||||
INVALID_MESSAGE_ID = 'شناسه پیام نامعتبر است',
|
||||
REQUIRED_FIELD_MISSING = 'فیلد ضروری موجود نیست',
|
||||
|
||||
// Success messages
|
||||
AUTHENTICATED = 'احراز هویت موفقیتآمیز',
|
||||
SESSION_CREATED = 'جلسه چت با موفقیت ایجاد شد',
|
||||
CHAT_JOINED = 'با موفقیت به چت متصل شدید',
|
||||
MESSAGE_SENT = 'پیام ارسال شد',
|
||||
CONNECTION_ESTABLISHED = 'اتصال برقرار شد',
|
||||
}
|
||||
|
||||
export const enum SmsMessage {
|
||||
SEND_SMS_ERROR = 'خطا در ارسال پیامک',
|
||||
}
|
||||
|
||||
export const enum RestMessage {
|
||||
NOT_FOUND = 'کسب و کار یافت نشد',
|
||||
BUSINESS_ID_REQUIRED = 'شناسه کسب و کار مورد نیاز است',
|
||||
SLUG_REQUIRED = 'اسلاگ مورد نیاز است',
|
||||
SLUG_MUST_BE_STRING = 'اسلاگ باید یک رشته باشد',
|
||||
DOMAIN_INVALID = 'دامنه وارد شده معتبر نیست',
|
||||
DOMAIN_UPDATED = 'دامنه با موفقیت بهروزرسانی شد',
|
||||
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
|
||||
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
|
||||
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
|
||||
DOMAIN_VERIFICATION_METHOD_INVALID = 'روش تایید دامنه نامعتبر است',
|
||||
DOMAIN_ALREADY_VERIFIED = 'این دامنه قبلا تایید شده است',
|
||||
DOMAIN_REQUIRED = 'دامنه مورد نیاز است',
|
||||
DOMAIN_MUST_BE_STRING = 'دامنه باید یک رشته باشد',
|
||||
STAFF_NOT_FOUND = 'کاربر ادمین یافت نشد',
|
||||
ZARINPAL_MERCHANT_ID_REQUIRED = 'شناسه مرچنت زرین پال مورد نیاز است',
|
||||
ZARINPAL_MERCHANT_ID_STRING = 'شناسه مرچنت زرین پال باید یک رشته باشد',
|
||||
LOGO_URL_REQUIRED = 'آدرس تصویر لوگو مورد نیاز است',
|
||||
LOGO_URL_STRING = 'آدرس تصویر لوگو باید یک رشته باشد',
|
||||
LOGO_URL_INVALID = 'آدرس تصویر لوگو معتبر نیست',
|
||||
BUSINESS_SETTINGS_UPDATED = 'تنظیمات کسب و کار با موفقیت به روز رسانی شد',
|
||||
NAME_REQUIRED = 'نام کسب و کار مورد نیاز است',
|
||||
NAME_STRING = 'نام کسب و کار باید یک رشته باشد',
|
||||
DOMAIN_CONNECTED = 'دامنه با موفقیت متصل شد',
|
||||
DOMAIN_NOT_CONNECTED = 'دامنه به سرور ما متصل نشد',
|
||||
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
|
||||
DOMAIN_VERIFICATION_TOKEN_REQUIRED = 'توکن تایید دامنه مورد نیاز است',
|
||||
INSUFFICIENT_QUOTA = 'حجم شما برای ساخت ایمیل کافی نمی باشد، لطفا حجم را افزایش دهید',
|
||||
QUOTA_DOWNGRADE_NOT_ALLOWED = 'کاهش حجم ایمیل مجاز نیست، فقط میتوانید حجم را افزایش دهید',
|
||||
QUOTA_AMOUNT_REQUIRED = 'مقدار حجم مورد نیاز است',
|
||||
QUOTA_AMOUNT_MUST_BE_NUMBER = 'مقدار حجم باید یک عدد باشد',
|
||||
QUOTA_AMOUNT_MUST_BE_POSITIVE = 'مقدار حجم باید مثبت باشد',
|
||||
CALLBACK_URL_MUST_BE_STRING = 'آدرس بازگشت باید یک رشته باشد',
|
||||
CALLBACK_URL_INVALID = 'آدرس بازگشت معتبر نیست',
|
||||
PURCHASE_DESCRIPTION_MUST_BE_STRING = 'توضیحات خرید باید یک رشته باشد',
|
||||
QUOTA_PURCHASE_SUCCESS = 'درخواست خرید حجم با موفقیت ثبت شد',
|
||||
QUOTA_PURCHASE_FAILED = 'خرید حجم با خطا مواجه شد',
|
||||
PAYMENT_GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
|
||||
QUOTA_UPDATED = 'حجم ایمیل با موفقیت به روز شد',
|
||||
USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = 'کاربر دسترسی به این سرویس را ندارد',
|
||||
}
|
||||
|
||||
export const enum DomainMessage {
|
||||
// Domain Ownership & Verification
|
||||
NOT_BELONG_TO_BUSINESS = 'دامنه به این کسب و کار تعلق ندارد',
|
||||
NOT_VERIFIED = 'دامنه باید تایید شده باشد',
|
||||
DOMAIN_NOT_FOUND = 'دامنه یافت نشد',
|
||||
DOMAIN_ALREADY_EXISTS = 'دامنه [name] قبلا ثبت شده است',
|
||||
DOMAIN_DELETED_SUCCESSFULLY = 'دامنه با موفقیت حذف شد',
|
||||
VERIFICATION_NOT_FOUND = 'تایید دامنه یافت نشد',
|
||||
NO_DOMAIN_FOUND = 'دامنه ای یافت نشد',
|
||||
DOMAIN_CHECKED = 'دامنه با موفقیت بررسی شد',
|
||||
|
||||
// Domain Creation & Setup
|
||||
DOMAIN_CREATED_WITH_RECORDS = 'دامنه [name] با موفقیت ایجاد شد و تمام رکوردهای مورد نیاز تولید گردید',
|
||||
ALL_REQUIRED_RECORDS_GENERATED = 'تمام رکوردهای مورد نیاز (MX, SPF, DMARC, DKIM) به طور خودکار برای شما تولید شدهاند',
|
||||
ESTIMATED_PROPAGATION_TIME = 'تغییرات DNS معمولاً ۱۵ دقیقه تا ۴۸ ساعت در سراسر جهان منتشر میشود',
|
||||
|
||||
// DNS Records Instructions
|
||||
ADD_MX_RECORD = 'یک رکورد MX با نام [name] که به [value] با اولویت [priority] اشاره کند اضافه کنید',
|
||||
ADD_TXT_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید',
|
||||
ADD_DKIM_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (کلید عمومی DKIM)',
|
||||
ADD_DMARC_RECORD = 'یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (سیاست DMARC)',
|
||||
ADD_CNAME_RECORD = 'یک رکورد CNAME با نام [name] که به [value] اشاره کند اضافه کنید',
|
||||
ADD_GENERIC_RECORD = 'یک رکورد [type] با نام [name] و مقدار [value] اضافه کنید',
|
||||
|
||||
// Domain Setup Steps
|
||||
STEP_LOGIN_DNS_PANEL = 'وارد پنل مدیریت DNS ثبت کننده دامنه خود شوید',
|
||||
STEP_ADD_ALL_RECORDS = 'تمام رکوردهای DNS مورد نیاز لیست شده در بالا را به زون DNS دامنه خود اضافه کنید',
|
||||
STEP_INCLUDE_ALL_TYPES = 'مطمئن شوید که رکوردهای MX، SPF، DMARC و DKIM را برای عملکرد کامل ایمیل شامل کردهاید',
|
||||
STEP_WAIT_PROPAGATION = 'منتظر انتشار DNS بمانید (معمولاً ۱۵ دقیقه تا ۴۸ ساعت)',
|
||||
STEP_USE_CHECK_ENDPOINT = "از نقطه پایانی 'بررسی DNS' برای تایید تنظیم صحیح رکوردهای خود استفاده کنید",
|
||||
STEP_DOMAIN_READY = 'پس از تایید، دامنه شما برای سرویسهای ایمیل آماده خواهد بود!',
|
||||
|
||||
// Status Messages
|
||||
RECORDS_PENDING_VERIFICATION = '[count] رکورد DNS هنوز در انتظار تایید هستند',
|
||||
ALL_REQUIRED_RECORDS_MUST_BE_CONFIGURED = 'تمام [count] رکورد مورد نیاز باید برای کار درست ایمیل تنظیم شوند',
|
||||
DOMAIN_FULLY_VERIFIED = 'دامنه شما به طور کامل تایید شده و برای ایمیل آماده است!',
|
||||
EMAIL_ACCOUNTS_READY = 'اکنون میتوانید حسابهای ایمیل ایجاد کرده و شروع به ارسال/دریافت ایمیل کنید',
|
||||
|
||||
// Verification Status
|
||||
DOMAIN_VERIFICATION_INITIATED = 'فرآیند تایید دامنه آغاز شد',
|
||||
DOMAIN_VERIFICATION_FAILED = 'تایید دامنه با خطا مواجه شد',
|
||||
DOMAIN_VERIFICATION_SUCCESS = 'دامنه با موفقیت تایید شد',
|
||||
DOMAIN_VERIFICATION_PENDING = 'تایید دامنه در انتظار است',
|
||||
|
||||
// DNS Record Status
|
||||
DNS_RECORD_VERIFIED = 'رکورد DNS تایید شد',
|
||||
DNS_RECORD_FAILED = 'رکورد DNS تایید نشد',
|
||||
DNS_RECORD_NOT_FOUND_OR_MISMATCH = 'رکورد DNS یافت نشد یا مقدار مطابقت ندارد',
|
||||
|
||||
// DKIM Messages
|
||||
DKIM_ENABLED_AUTOMATICALLY = 'DKIM به طور خودکار برای دامنه [name] فعال شد',
|
||||
DKIM_FAILED_TO_CREATE = 'ایجاد DKIM برای دامنه [name] ناموفق بود',
|
||||
DKIM_ENABLED_SUCCESSFULLY = 'DKIM برای دامنه [name] با موفقیت فعال شد',
|
||||
|
||||
// Error Messages
|
||||
FAILED_TO_VERIFY_DOMAIN = 'تایید دامنه [name] ناموفق بود',
|
||||
FAILED_TO_SETUP_MAIL_SERVER = 'راهاندازی دامنه [name] در سرور ایمیل ناموفق بود',
|
||||
DOMAIN_CREATION_FAILED = 'ایجاد دامنه ناموفق بود',
|
||||
DNS_VERIFICATION_ATTEMPTS_EXCEEDED = 'تعداد تلاشهای تایید DNS از حد مجاز بیشتر شد',
|
||||
|
||||
// Service Messages
|
||||
DOMAIN_UPDATED_SUCCESSFULLY = 'دامنه بروزرسانی شد',
|
||||
VERIFICATION_TOKEN_PREFIX = 'توکن تایید: [token]',
|
||||
RECORD_NEEDS_FIXING = 'رکورد [type] برای [name] نیاز به اصلاح دارد: [error]',
|
||||
RECORD_PENDING_VERIFICATION_DETAIL = 'رکورد [type] برای [name] هنوز در انتظار تایید است. لطفا مطمئن شوید که انتشار DNS کامل شده است.',
|
||||
RECORD_DESCRIPTION_EMAIL_FUNCTIONALITY = 'رکورد [type] برای عملکرد ایمیل',
|
||||
BUSINESS_ALREADY_HAS_DOMAIN = 'این کسب و کار قبلا دامنه ای دارد',
|
||||
CAN_NOT_USE_THIS_DOMAIN = 'این دامنه نمیتواند ثبت شود',
|
||||
}
|
||||
|
||||
export const enum MailServerMessage {
|
||||
FAILED_TO_CREATE_ACCOUNT = 'خطا در ایجاد ایمیل',
|
||||
FAILED_TO_CREATE_DKIM_KEY = 'خطا در ایجاد کلید DKIM',
|
||||
}
|
||||
|
||||
export const enum DnsRecordMessage {
|
||||
DNS_RECORD_NOT_FOUND = 'رکورد DNS یافت نشد',
|
||||
DNS_RECORD_DELETED = 'رکورد DNS با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum RoleMessage {
|
||||
NOT_FOUND = 'نقش یافت نشد',
|
||||
ROLE_HAS_ADMINS = 'نقش دارای ادمین است و قابل حذف نیست',
|
||||
}
|
||||
|
||||
export const enum SignatureMessage {
|
||||
NAME_NOT_EMPTY = 'نام امضاء نمیتواند خالی باشد',
|
||||
NAME_MAX_LENGTH = 'نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
|
||||
NAME_STRING = 'نام امضاء باید یک رشته باشد',
|
||||
TEXT_CONTENT_NOT_EMPTY = 'محتوای متنی امضاء نمیتواند خالی باشد',
|
||||
TEXT_CONTENT_STRING = 'محتوای متنی امضاء باید یک رشته باشد',
|
||||
TEXT_CONTENT_MAX_LENGTH = 'محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد',
|
||||
IS_ACTIVE_NOT_EMPTY = 'وضعیت فعال بودن نمیتواند خالی باشد',
|
||||
IS_ACTIVE_BOOLEAN = 'وضعیت فعال بودن باید بولین باشد',
|
||||
IS_DEFAULT_NOT_EMPTY = 'وضعیت پیش فرض نمیتواند خالی باشد',
|
||||
IS_DEFAULT_BOOLEAN = 'وضعیت پیش فرض باید بولین باشد',
|
||||
CREATED = 'امضاء با موفقیت ایجاد شد',
|
||||
NOT_FOUND = 'امضاء یافت نشد',
|
||||
DELETED = 'امضاء با موفقیت حذف شد',
|
||||
TOGGLE_ACTIVE = 'وضعیت امضاء با موفقیت تغییر کرد',
|
||||
}
|
||||
|
||||
export const enum TemplateMessage {
|
||||
TEMPLATE_NOT_FOUND = 'قالب یافت نشد',
|
||||
TEMPLATE_CREATED_SUCCESSFULLY = 'قالب با موفقیت ایجاد شد',
|
||||
TEMPLATE_UPDATED_SUCCESSFULLY = 'قالب با موفقیت به روزرسانی شد',
|
||||
TEMPLATE_DELETED_SUCCESSFULLY = 'قالب با موفقیت حذف شد',
|
||||
TEMPLATE_RETRIEVED_SUCCESSFULLY = 'قالب با موفقیت دریافت شد',
|
||||
TEMPLATES_LISTED_SUCCESSFULLY = 'لیست قالب ها با موفقیت دریافت شد',
|
||||
TEMPLATE_NAME_REQUIRED = 'نام قالب الزامی است',
|
||||
TEMPLATE_NAME_STRING = 'نام قالب باید یک رشته باشد',
|
||||
TEMPLATE_NAME_MAX_LENGTH = 'نام قالب نمیتواند بیشتر از ۲۵۵ کاراکتر باشد',
|
||||
TEMPLATE_STRUCTURE_REQUIRED = 'ساختار قالب الزامی است',
|
||||
TEMPLATE_STRUCTURE_VALID = 'ساختار قالب باید معتبر باشد',
|
||||
TEMPLATE_RAW_HTML_STRING = 'HTML خام باید یک رشته باشد',
|
||||
TEMPLATE_BUSINESS_ID_REQUIRED = 'شناسه کسب و کار الزامی است',
|
||||
TEMPLATE_BUSINESS_ID_UUID = 'شناسه کسب و کار باید یک UUID معتبر باشد',
|
||||
TEMPLATE_ID_REQUIRED = 'شناسه قالب الزامی است',
|
||||
TEMPLATE_ID_UUID = 'شناسه قالب باید یک UUID معتبر باشد',
|
||||
TEMPLATE_ACCESS_DENIED = 'شما دسترسی به این قالب را ندارید',
|
||||
TEMPLATE_BUSINESS_NOT_FOUND = 'کسب و کار مالک قالب یافت نشد',
|
||||
SEARCH_STRING = 'نام برای جستجو باید یک رشته باشد',
|
||||
TEMPLATE_SELECTED_SUCCESSFULLY = 'وضعیت قالب با موفقیت تغییر کرد',
|
||||
TEMPLATE_THUMBNAIL_URL_VALID = 'آدرس تصویر قالب باید یک آدرس معتبر باشد',
|
||||
TEMPLATE_RAW_HTML_REQUIRED = 'HTML خام قالب الزامی است',
|
||||
}
|
||||
|
||||
export const enum NotificationMessage {
|
||||
TITLE_IS_REQUIRED = 'عنوان اعلان الزامی است',
|
||||
TITLE_STRING = 'عنوان اعلان باید یک رشته باشد',
|
||||
MESSAGE_IS_REQUIRED = 'متن اعلان الزامی است',
|
||||
MESSAGE_STRING = 'متن اعلان باید یک رشته باشد',
|
||||
USERID_IS_REQUIRED = 'شناسه کاربر الزامی است',
|
||||
USERID_IS_STRING = 'شناسه کاربر باید یک رشته باشد',
|
||||
USERID_IS_UUID = 'شناسه کاربر باید یک UUID معتبر باشد',
|
||||
TYPE_IS_REQUIRED = 'نوع اعلان الزامی است',
|
||||
TYPE_ENUM = 'نوع اعلان باید یک enum معتبر باشد',
|
||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
|
||||
LOGIN_MESSAGE = 'ورود با موفقیت انجام شد در تاریخ [loginDate]',
|
||||
LOGIN = 'ورود',
|
||||
NEW_EMAIL = 'ایمیل جدید',
|
||||
NEW_EMAIL_MESSAGE = 'یک ایمیل جدید با موضوع [subject] دریافت شد',
|
||||
CHANGE_PASSWORD_MESSAGE = 'رمز عبور شما با موفقیت تغییر کرد',
|
||||
CHANGE_PASSWORD = 'تغییر رمز عبور',
|
||||
NOT_FOUND = 'اعلان یافت نشد',
|
||||
READ_SUCCESS = 'اعلان با موفقیت خوانده شد',
|
||||
ORDER_CREATED = 'سفارش جدید ایجاد شد',
|
||||
ORDER_STATUS_CHANGED = 'وضعیت سفارش تغییر کرد',
|
||||
PAYMENT_SUCCESS = 'پرداخت با موفقیت انجام شد',
|
||||
PAGER_CREATED = 'پیج جدید ایجاد شد',
|
||||
}
|
||||
|
||||
export const enum SettingMessageEnum {
|
||||
SMS_MUST_BE_BOOLEAN = 'تنظیمات ارسال پیامک باید یک boolean باشد',
|
||||
EMAIL_MUST_BE_BOOLEAN = 'تنظیمات ارسال ایمیل باید یک boolean باشد',
|
||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = 'اعلان یافت نشد یا دسترسی آن را ندارید',
|
||||
}
|
||||
|
||||
export const enum AiMessage {
|
||||
NO_RESPONSE_FROM_AI_MODEL = 'خطا در دریافت پاسخ از مدل AI',
|
||||
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = 'ایمیل یافت نشد یا دسترسی آن را ندارید',
|
||||
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = 'محتوای ایمیل خالی است یا قابل استخراج نیست',
|
||||
DESCRIPTION_IS_REQUIRED = 'شرح و هدف قالب الزامی است',
|
||||
DESCRIPTION_MUST_BE_A_STRING = 'شرح قالب باید یک رشته باشد',
|
||||
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = 'شرح قالب باید حداقل ۱۰ کاراکتر باشد',
|
||||
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = 'شرح قالب نمیتواند بیشتر از ۵۰۰ کاراکتر باشد',
|
||||
THEME_MUST_BE_A_VALID_VALUE = 'تم قالب باید یکی از مقادیر مجاز باشد',
|
||||
STYLE_MUST_BE_A_VALID_VALUE = 'سبک قالب باید یکی از مقادیر مجاز باشد',
|
||||
PURPOSE_MUST_BE_A_VALID_VALUE = 'هدف قالب باید یکی از مقادیر مجاز باشد',
|
||||
TEMPLATE_NAME_MUST_BE_A_STRING = 'نام قالب باید یک رشته باشد',
|
||||
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = 'نام قالب نمیتواند بیشتر از ۱۰۰ کاراکتر باشد',
|
||||
PREFERRED_COLORS_MUST_BE_A_STRING = 'رنگهای ترجیحی باید یک رشته باشد',
|
||||
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = 'رنگهای ترجیحی نمیتواند بیشتر از ۲۰۰ کاراکتر باشد',
|
||||
PREFERRED_COLORS_IS_REQUIRED = 'رنگهای ترجیحی الزامی است',
|
||||
}
|
||||
|
||||
export const enum TwoFactorMessage {
|
||||
TWO_FACTOR_SETUP_INITIATED = 'راهاندازی احراز هویت دو مرحلهای با موفقیت آغاز شد',
|
||||
TWO_FACTOR_ENABLED_SUCCESSFULLY = 'احراز هویت دو مرحلهای با موفقیت فعال شد',
|
||||
TWO_FACTOR_DISABLED_SUCCESSFULLY = 'احراز هویت دو مرحلهای با موفقیت غیرفعال شد',
|
||||
TWO_FACTOR_SETUP_FAILED = 'راهاندازی احراز هویت دو مرحلهای ناموفق بود',
|
||||
TWO_FACTOR_ENABLE_FAILED = 'فعالسازی احراز هویت دو مرحلهای ناموفق بود',
|
||||
TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY = 'کد احراز هویت دو مرحلهای با موفقیت تأیید شد',
|
||||
TWO_FACTOR_TOKEN_VERIFIED_FAILED = 'کد احراز هویت دو مرحلهای نامعتبر میباشد',
|
||||
TWO_FACTOR_NOT_ENABLED = 'احراز هویت دو مرحلهای فعال نیست',
|
||||
TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY = 'کدهای پشتیبان با موفقیت تولید شدند',
|
||||
TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY = 'کد پشتیبان با موفقیت تأیید شد',
|
||||
TWO_FACTOR_BACKUP_CODE_VERIFIED_FAILED = 'کد پشتیبان نامعتبر است',
|
||||
TWO_FACTOR_TOKEN_REQUIRED = 'کد احراز هویت دو مرحلهای الزامی است',
|
||||
TWO_FACTOR_TOKEN_INVALID = 'کد احراز هویت دو مرحلهای نامعتبر است',
|
||||
}
|
||||
|
||||
export const enum CouponMessage {
|
||||
NOT_FOUND = 'کوپن یافت نشد',
|
||||
CODE_ALREADY_EXISTS = 'کد کوپن قبلاً ثبت شده است',
|
||||
END_DATE_MUST_BE_AFTER_START_DATE = 'تاریخ پایان باید بعد از تاریخ شروع باشد',
|
||||
PERCENTAGE_MUST_BE_BETWEEN_0_AND_100 = 'درصد تخفیف باید بین ۰ تا ۱۰۰ باشد',
|
||||
COUPON_INACTIVE = 'کوپن غیرفعال است',
|
||||
COUPON_NOT_STARTED = 'کوپن هنوز شروع نشده است',
|
||||
COUPON_EXPIRED = 'کوپن منقضی شده است',
|
||||
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
|
||||
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز است',
|
||||
COUPON_CREATED = 'کوپن با موفقیت ایجاد شد',
|
||||
COUPON_UPDATED = 'کوپن با موفقیت بهروزرسانی شد',
|
||||
COUPON_DELETED = 'کوپن با موفقیت حذف شد',
|
||||
COUPON_VALID = 'کوپن معتبر است',
|
||||
COUPON_INVALID = 'کوپن نامعتبر است',
|
||||
COUPON_RESTRICTED_TO_USER = 'کوپن فقط برای کاربر مشخصی معتبر است',
|
||||
}
|
||||
|
||||
export const enum ReviewMessage {
|
||||
NOT_FOUND = 'نظر یافت نشد',
|
||||
ALREADY_COMMENTED = 'شما قبلاً برای این غذا نظر دادهاید',
|
||||
CAN_ONLY_UPDATE_OWN = 'شما فقط میتوانید نظرات خود را ویرایش کنید',
|
||||
CAN_ONLY_DELETE_OWN = 'شما فقط میتوانید نظرات خود را حذف کنید',
|
||||
RATING_REQUIRED = 'امتیاز الزامی است',
|
||||
RATING_INVALID = 'امتیاز باید بین ۱ تا ۵ باشد',
|
||||
COMMENT_CREATED = 'نظر با موفقیت ثبت شد',
|
||||
COMMENT_UPDATED = 'نظر با موفقیت بهروزرسانی شد',
|
||||
COMMENT_DELETED = 'نظر با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
export const enum IconMessage {
|
||||
NOT_FOUND = 'آیکون یافت نشد',
|
||||
NOT_CREATED = 'ایجاد آیکون با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی آیکون با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف آیکون با خطا مواجه شد',
|
||||
}
|
||||
|
||||
export const enum GroupMessage {
|
||||
NOT_FOUND = 'گروه آیکون یافت نشد',
|
||||
NOT_CREATED = 'ایجاد گروه آیکون با خطا مواجه شد',
|
||||
NOT_UPDATED = 'بهروزرسانی گروه آیکون با خطا مواجه شد',
|
||||
NOT_DELETED = 'حذف گروه آیکون با خطا مواجه شد',
|
||||
}
|
||||
|
||||
export const enum OrderMessage {
|
||||
NOT_FOUND = 'سفارش یافت نشد',
|
||||
PAYMENT_METHOD_MISSING = 'روش پرداخت سفارش مشخص نشده است',
|
||||
INVALID_STATUS_TRANSITION = 'انتقال وضعیت نامعتبر است',
|
||||
CART_EMPTY = 'سبد خرید خالی است. لطفا قبل از ایجاد سفارش، آیتمهایی به سبد اضافه کنید',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
PAYMENT_METHOD_REQUIRED = 'روش پرداخت الزامی است. لطفا قبل از ایجاد سفارش، روش پرداخت را تنظیم کنید',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
DELIVERY_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
MIN_ORDER_AMOUNT_NOT_MET = 'مبلغ سفارش کمتر از حداقل مجاز برای این روش ارسال است',
|
||||
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
|
||||
ADDRESS_REQUIRED = 'آدرس الزامی است. لطفا قبل از ایجاد سفارش، آدرس تحویل را تنظیم کنید',
|
||||
CAR_ADDRESS_REQUIRED = 'آدرس خودرو الزامی است. لطفا قبل از ایجاد سفارش، آدرس خودرو را تنظیم کنید',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
}
|
||||
|
||||
export const enum CartMessage {
|
||||
TABLE_NUMBER_REQUIRED = 'شماره میز برای سفارش در محل الزامی است',
|
||||
CAR_ADDRESS_REQUIRED = 'آدرس خودرو برای تحویل به خودرو الزامی است',
|
||||
ADDRESS_REQUIRED = 'آدرس برای تحویل پیک الزامی است',
|
||||
NOT_FOUND = 'سبد خرید یافت نشد',
|
||||
COUPON_NOT_FOUND = 'کوپن یافت نشد',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
FOOD_NO_INVENTORY = 'غذا موجودی ندارد',
|
||||
INSUFFICIENT_STOCK = 'موجودی کافی نیست',
|
||||
USER_NOT_FOUND = 'کاربر یافت نشد',
|
||||
ADDRESS_NOT_FOUND = 'آدرس یافت نشد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
ADDRESS_NOT_BELONGS_TO_USER = 'آدرس به کاربر فعلی تعلق ندارد',
|
||||
ADDRESS_NO_COORDINATES = 'آدرس مختصات جغرافیایی ندارد',
|
||||
ADDRESS_OUTSIDE_SERVICE_AREA = 'آدرس خارج از محدوده سرویس رستوران است',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
DELIVERY_METHOD_NOT_ENABLED = 'روش ارسال برای این رستوران فعال نیست',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
PAYMENT_METHOD_NOT_ENABLED = 'روش پرداخت برای این رستوران فعال نیست',
|
||||
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
|
||||
WALLET_INSUFFICIENT = 'موجودی کیف پول کافی نیست',
|
||||
COUPON_MAX_USES_REACHED = 'حداکثر استفاده از کوپن به پایان رسیده است',
|
||||
ITEM_NOT_FOUND = 'آیتم در سبد خرید یافت نشد',
|
||||
DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT = 'روش ارسال برای این رستوران یافت نشد',
|
||||
COUPON_CANNOT_BE_APPLIED = 'این کوپن قابل اعمال بر روی آیتمهای سبد خرید شما نیست. لطفا آیتمهایی از دستهبندیها یا غذاهای مشخص شده اضافه کنید',
|
||||
FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER = 'تمام غذاها باید قابلیت تحویل پیک داشته باشند. غذاهای زیر این قابلیت را ندارند: [foodTitles]',
|
||||
FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES = 'غذا [foodTitle] فقط در ساعات وعدههای غذایی (صبحانه، ناهار، عصرانه یا شام) در دسترس است. زمان فعلی خارج از ساعات وعدههای غذایی است',
|
||||
FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES = 'غذا [foodTitle] فقط برای [allowedMealTypes] در دسترس است. زمان فعلی [mealType] است که با این غذا سازگار نیست',
|
||||
FOOD_ONLY_AVAILABLE_ON_DAYS = 'غذا [foodTitle] فقط در روزهای [allowedDays] در دسترس است. امروز [currentDay] است که این غذا در دسترس نیست',
|
||||
}
|
||||
|
||||
export const enum PaymentMessage {
|
||||
UNSUPPORTED_PAYMENT_METHOD = 'روش پرداخت پشتیبانی نمیشود',
|
||||
ORDER_NOT_FOUND = 'سفارش یافت نشد',
|
||||
AMOUNT_MUST_BE_GREATER_THAN_ZERO = 'مبلغ باید بیشتر از صفر باشد',
|
||||
PAYMENT_METHOD_NOT_FOUND = 'روش پرداخت یافت نشد',
|
||||
GATEWAY_REQUIRED = 'درگاه پرداخت برای پرداخت آنلاین الزامی است',
|
||||
MERCHANT_ID_REQUIRED = 'شناسه مرچنت برای پرداخت آنلاین الزامی است',
|
||||
RESTAURANT_DOMAIN_REQUIRED = 'دامنه رستوران برای پرداخت آنلاین الزامی است',
|
||||
WALLET_NOT_FOUND = 'کیف پول کاربر یافت نشد',
|
||||
INSUFFICIENT_WALLET_BALANCE = 'موجودی کیف پول کافی نیست',
|
||||
PAYMENT_NOT_FOUND = 'پرداخت یافت نشد',
|
||||
INVALID_PAYMENT_CONFIGURATION = 'پیکربندی پرداخت نامعتبر است',
|
||||
PAYMENT_METHOD_NOT_CASH = 'روش پرداخت نقدی نیست',
|
||||
PAYMENT_ALREADY_PAID = 'پرداخت قبلا انجام شده است',
|
||||
EXISTING_PENDING_PAYMENT_MISMATCH = 'پرداخت در انتظار موجود با روش پرداخت سفارش مطابقت ندارد',
|
||||
ZARINPAL_INVALID_RESPONSE = 'پاسخ نامعتبر از API زرینپال',
|
||||
ZARINPAL_PAYMENT_REQUEST_ERROR = 'خطا در درخواست پرداخت زرینپال',
|
||||
ZARINPAL_VERIFY_ERROR = 'خطا در تایید پرداخت زرینپال',
|
||||
GATEWAY_ERROR = 'خطا در اتصال به درگاه پرداخت',
|
||||
}
|
||||
|
||||
export const enum DeliveryMessage {
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
DELIVERY_METHOD_NOT_FOUND = 'روش ارسال یافت نشد',
|
||||
}
|
||||
|
||||
export const enum InventoryMessage {
|
||||
AVAILABLE_STOCK_EXCEEDS_TOTAL = 'موجودی موجود نمیتواند از موجودی کل بیشتر باشد',
|
||||
FOOD_NOT_FOUND = 'غذا یافت نشد',
|
||||
FOOD_NOT_BELONGS_TO_RESTAURANT = 'غذا به این رستوران تعلق ندارد',
|
||||
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
|
||||
FOODS_NOT_FOUND = 'غذاها یافت نشدند',
|
||||
}
|
||||
|
||||
|
||||
export const enum PagerMessage {
|
||||
CREATED_SUCCESS = 'درخواست پیج با موفقیت ایجاد شد',
|
||||
}
|
||||
|
||||
export const enum UserSuccessMessage {
|
||||
ADDRESS_DELETED_SUCCESS = 'آدرس با موفقیت حذف شد',
|
||||
SCORE_CONVERTED_SUCCESS = 'امتیاز با موفقیت به کیف پول تبدیل شد',
|
||||
}
|
||||
|
||||
export const enum ContactMessage {
|
||||
NOT_FOUND = 'تماس یافت نشد',
|
||||
CREATED = 'تماس با موفقیت ثبت شد',
|
||||
UPDATED = 'تماس با موفقیت بهروزرسانی شد',
|
||||
DELETED = 'تماس با موفقیت حذف شد',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Permission names enum
|
||||
* All permission names used in the system should be defined here
|
||||
*/
|
||||
export enum Permission {
|
||||
MANAGE_PAGER = 'manage_pager',
|
||||
// Food Management
|
||||
MANAGE_FOODS = 'manage_foods',
|
||||
MANAGE_CATEGORIES = 'manage_categories',
|
||||
|
||||
// Order Management
|
||||
MANAGE_ORDERS = 'manage_orders',
|
||||
|
||||
// User & Admin Management
|
||||
MANAGE_ADMINS = 'manage_admins',
|
||||
MANAGE_USERS = 'manage_users',
|
||||
|
||||
// Reports & Finances
|
||||
VIEW_REPORTS = 'view_reports',
|
||||
|
||||
|
||||
UPDATE_RESTAURANT = 'update_restaurant',
|
||||
|
||||
// Role Management
|
||||
MANAGE_ROLES = 'manage_roles',
|
||||
MANAGE_REVIEWS = 'manage_reviews',
|
||||
MANAGE_COUPONS = 'manage_coupons',
|
||||
MANAGE_PAYMENTS = 'manage_payments',
|
||||
MANAGE_DELIVERY = 'manage_delivery',
|
||||
MANAGE_SETTINGS = 'manage_settings',
|
||||
MANAGE_SCHEDULES = 'manage_schedules',
|
||||
MANAGE_REPORTS = 'manage_reports',
|
||||
MANAGE_CONTACTS = 'manage_contacts',
|
||||
|
||||
NEW_ORDER_NOTIFICATION='new_order_notification',
|
||||
PAGER_NOTIFICATION='pager_notification'
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission titles mapping (Farsi)
|
||||
* Maps permission names to their Farsi titles
|
||||
*/
|
||||
export const PermissionTitles: Record<Permission, string> = {
|
||||
[Permission.MANAGE_PAGER]: 'مدیریت پیجر',
|
||||
[Permission.MANAGE_FOODS]: 'مدیریت غذاها',
|
||||
[Permission.MANAGE_CATEGORIES]: 'مدیریت دستهبندیها',
|
||||
[Permission.MANAGE_ORDERS]: 'مدیریت سفارشات',
|
||||
[Permission.MANAGE_ADMINS]: 'مدیریت مدیران',
|
||||
[Permission.MANAGE_USERS]: 'مدیریت کاربران',
|
||||
[Permission.VIEW_REPORTS]: 'مشاهده گزارشات',
|
||||
[Permission.UPDATE_RESTAURANT]: 'ویرایش رستوران',
|
||||
[Permission.MANAGE_ROLES]: 'مدیریت نقشها',
|
||||
[Permission.MANAGE_REVIEWS]: 'مدیریت نظرات',
|
||||
[Permission.MANAGE_COUPONS]: 'مدیریت کوپنها',
|
||||
[Permission.MANAGE_PAYMENTS]: 'مدیریت پرداختها',
|
||||
[Permission.MANAGE_DELIVERY]: 'مدیریت تحویل',
|
||||
[Permission.MANAGE_SETTINGS]: 'مدیریت تنظیمات',
|
||||
[Permission.MANAGE_SCHEDULES]: 'مدیریت برنامهها',
|
||||
[Permission.MANAGE_REPORTS]: 'مدیریت گزارشات',
|
||||
[Permission.MANAGE_CONTACTS]: 'مدیریت تماسهای کاربران',
|
||||
[Permission.NEW_ORDER_NOTIFICATION]: "نوتیف سفارش جدید",
|
||||
[Permission.PAGER_NOTIFICATION]: "نوتیف پیجر جدید"
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ValueProvider } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { MIKRO_ORM_QUERY_LOGGER } from '../constants';
|
||||
|
||||
export const mikroOrmQueryLoggerProvider: ValueProvider = {
|
||||
provide: MIKRO_ORM_QUERY_LOGGER, //
|
||||
useValue: new Logger('mikro-orm'),
|
||||
};
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import { Keyv } from 'keyv';
|
||||
import KeyvRedis from '@keyv/redis';
|
||||
import type { CacheModuleAsyncOptions } from '@nestjs/cache-manager';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CacheableMemory } from 'cacheable';
|
||||
|
||||
export function cacheConfig(): CacheModuleAsyncOptions {
|
||||
return {
|
||||
isGlobal: true,
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const redisUri = configService.get('REDIS_URI');
|
||||
const namespace = configService.get('CACHE_NAMESPACE', 'app-cache');
|
||||
const ttl = +configService.get<number>('CACHE_TTL', 3600); //1 hour
|
||||
|
||||
return {
|
||||
stores: [
|
||||
new Keyv({
|
||||
store: new CacheableMemory({ ttl: ttl * 1000, lruSize: 5000 }),
|
||||
namespace: namespace
|
||||
}),
|
||||
new KeyvRedis(redisUri, { namespace: namespace }),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import type { HttpModuleAsyncOptions } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export function httpConfig(): HttpModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
global: true,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
timeout: configService.getOrThrow<number>('AXIOS_TIMEOUT'),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
global: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { defineConfig, PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
// 1. Load environment variables from your .env file
|
||||
dotenv.config();
|
||||
|
||||
// 2. Read variables directly from process.env
|
||||
const DB_PASS = process.env.DB_PASS;
|
||||
const DB_USER = process.env.DB_USER;
|
||||
const DB_HOST = process.env.DB_HOST;
|
||||
const DB_PORT = process.env.DB_PORT ? +process.env.DB_PORT : 5432;
|
||||
const DB_NAME = process.env.DB_NAME;
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// 3. Add checks to ensure variables are loaded
|
||||
if (!DB_PASS || !DB_USER || !DB_HOST || !DB_PORT || !DB_NAME) {
|
||||
throw new Error('One or more database environment variables are not set.');
|
||||
}
|
||||
|
||||
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||
|
||||
// 4. Export the result of defineConfig as the default
|
||||
export default defineConfig({
|
||||
entities: ['dist/**/*.entity.js'],
|
||||
entitiesTs: ['src/**/*.entity.ts'],
|
||||
driver: PostgreSqlDriver,
|
||||
dbName: DB_NAME,
|
||||
debug: false,
|
||||
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||
ensureDatabase: isProduction
|
||||
? false
|
||||
: {
|
||||
forceCheck: true,
|
||||
create: true,
|
||||
schema: 'update',
|
||||
},
|
||||
forceUtcTimezone: true,
|
||||
pool: {
|
||||
min: isProduction ? 5 : 2,
|
||||
max: isProduction ? 20 : 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
acquireTimeoutMillis: 30000,
|
||||
reapIntervalMillis: 1000,
|
||||
createTimeoutMillis: 15000,
|
||||
destroyTimeoutMillis: 5000,
|
||||
},
|
||||
// 5. Use console for CLI logging, not the injected Nest logger
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
logger: isProduction ? console.log.bind(console) : console.debug.bind(console),
|
||||
schemaGenerator: {
|
||||
disableForeignKeys: false,
|
||||
},
|
||||
migrations: {
|
||||
path: './database/migrations',
|
||||
pathTs: './database/migrations',
|
||||
tableName: 'migrations',
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: 'ts',
|
||||
},
|
||||
connect: true,
|
||||
allowGlobalContext: true,
|
||||
seeder: {
|
||||
path: './dist/seeders',
|
||||
pathTs: './src/seeders',
|
||||
defaultSeeder: 'DatabaseSeeder',
|
||||
},
|
||||
driverOptions: {
|
||||
connection: {
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
statement_timeout: 60000,
|
||||
query_timeout: 60000,
|
||||
idle_in_transaction_session_timeout: 60000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
application_name: DB_NAME,
|
||||
},
|
||||
connectionRetries: 5,
|
||||
connectionRetryDelay: 3000,
|
||||
validateConnection: (connection: any) => {
|
||||
try {
|
||||
return !!(connection && !connection.closed);
|
||||
} catch (e: unknown) {
|
||||
// Use console.error directly
|
||||
console.error(`Connection validation failed: ${(e as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
poolErrorHandler: (err: Error) => {
|
||||
// Use console.error directly
|
||||
console.error(`PostgreSQL pool error: ${err.message}`);
|
||||
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||
console.warn('Connection reset detected, will attempt to reconnect');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { MikroOrmModuleAsyncOptions } from '@mikro-orm/nestjs';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||
// import { defineConfig } from '@mikro-orm/postgresql';
|
||||
|
||||
import type { Logger } from '@nestjs/common';
|
||||
import { MIKRO_ORM_QUERY_LOGGER } from '../common/constants';
|
||||
import { mikroOrmQueryLoggerProvider } from '../common/providers/mikro-orm-logger.provider';
|
||||
|
||||
const dataBaseConfig: MikroOrmModuleAsyncOptions = {
|
||||
// entities: ['dist/**/*.entity.js'],
|
||||
// entitiesTs: ['src/**/*.entity.ts'],
|
||||
// dbName: 'your_db_name',
|
||||
// type: 'postgresql',
|
||||
// user: 'your_user',
|
||||
// password: 'your_password',
|
||||
// host: 'localhost',
|
||||
// port: 5432,
|
||||
// debug: true,
|
||||
|
||||
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
||||
providers: [mikroOrmQueryLoggerProvider],
|
||||
useFactory: (configService: ConfigService, logger: Logger) => {
|
||||
const DB_PASS = configService.getOrThrow<string>('DB_PASS');
|
||||
const DB_USER = configService.getOrThrow<string>('DB_USER');
|
||||
const DB_HOST = configService.getOrThrow<string>('DB_HOST');
|
||||
const DB_PORT = configService.getOrThrow<number>('DB_PORT');
|
||||
const DB_NAME = configService.getOrThrow<string>('DB_NAME');
|
||||
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||
const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production';
|
||||
|
||||
return {
|
||||
// entities: ['dist/**/*.entity.js'],
|
||||
entitiesTs: ['src/**/*.entity.ts'],
|
||||
driver: PostgreSqlDriver,
|
||||
autoLoadEntities: true,
|
||||
dbName: DB_NAME,
|
||||
debug: false,
|
||||
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||
ensureDatabase: isProduction
|
||||
? false
|
||||
: {
|
||||
forceCheck: true,
|
||||
create: true,
|
||||
schema: 'update',
|
||||
},
|
||||
forceUtcTimezone: true,
|
||||
pool: {
|
||||
min: isProduction ? 5 : 2,
|
||||
max: isProduction ? 20 : 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
acquireTimeoutMillis: 30000,
|
||||
reapIntervalMillis: 1000,
|
||||
createTimeoutMillis: 15000,
|
||||
destroyTimeoutMillis: 5000,
|
||||
},
|
||||
logger: isProduction ? message => logger.log(message) : message => logger.debug(message),
|
||||
schemaGenerator: {
|
||||
// createForeignKey: !isProduction,
|
||||
disableForeignKeys: false,
|
||||
// createIndex: true,
|
||||
},
|
||||
migrations: {
|
||||
path: './database/migrations',
|
||||
pathTs: './database/migrations',
|
||||
tableName: 'migrations',
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: 'ts',
|
||||
},
|
||||
connect: true,
|
||||
allowGlobalContext: true,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
statement_timeout: 60000,
|
||||
query_timeout: 60000,
|
||||
idle_in_transaction_session_timeout: 60000,
|
||||
connectionTimeoutMillis: 10000,
|
||||
application_name: configService.getOrThrow<string>('DB_NAME'),
|
||||
},
|
||||
connectionRetries: 5,
|
||||
connectionRetryDelay: 3000,
|
||||
validateConnection: (connection: any) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return !!(connection && !connection.closed);
|
||||
} catch (e: unknown) {
|
||||
logger.error(`Connection validation failed: ${(e as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
poolErrorHandler: (err: Error) => {
|
||||
logger.error(`PostgreSQL pool error: ${err.message}`);
|
||||
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||
logger.warn('Connection reset detected, will attempt to reconnect');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default dataBaseConfig;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
|
||||
export function getSwaggerConfig(app: INestApplication) {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('D-menu API')
|
||||
.setDescription('API documentation for D-menu backend')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth() // optional: for JWT endpoints
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
SwaggerModule.setup('docs', app, document, {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
displayRequestDuration: true,
|
||||
filter: true,
|
||||
showExtensions: true,
|
||||
docExpansion: 'none',
|
||||
|
||||
// HIDE MODELS / SCHEMAS
|
||||
defaultModelsExpandDepth: -1,
|
||||
defaultModelExpandDepth: -1,
|
||||
},
|
||||
});
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
// Handle HTTP/REST context
|
||||
try {
|
||||
const ctx = host.switchToHttp();
|
||||
const reply = ctx.getResponse<FastifyReply>();
|
||||
|
||||
// Safety check: ensure reply has the status method
|
||||
if (!reply || typeof reply.status !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
// const request = ctx.getRequest<FastifyRequest>();
|
||||
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
const message = this.extractMessage(exceptionResponse);
|
||||
|
||||
reply.status(status).send({
|
||||
statusCode: status,
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
// timestamp: new Date().toISOString(),
|
||||
// path: request.url,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// If we can't handle it, let it propagate
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private extractMessage(response: string | Record<string, any>): string | string[] {
|
||||
if (typeof response === 'string') {
|
||||
return [response];
|
||||
}
|
||||
if (typeof response === 'object' && 'message' in response) {
|
||||
return Array.isArray(response.message) ? response.message : [response.message];
|
||||
}
|
||||
|
||||
return 'something wrong';
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||
// import { FastifyRequest } from 'fastify';
|
||||
// import { Observable, map } from 'rxjs';
|
||||
|
||||
// import { IPageFormat } from '../../../../danak_dsc_api/src/common/interfaces/IPagination';
|
||||
|
||||
// @Injectable()
|
||||
// export class PaginationInterceptor implements NestInterceptor {
|
||||
// private readonly logger = new Logger(PaginationInterceptor.name);
|
||||
|
||||
// intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const query = request.query as { page: string; limit: string };
|
||||
|
||||
// const page = parseInt(query.page, 10) || 1;
|
||||
// const limit = parseInt(query.limit, 10) || 10;
|
||||
|
||||
// const className = context.getClass().name;
|
||||
// const handlerName = context.getHandler().name;
|
||||
|
||||
// return next.handle().pipe(
|
||||
// map(data => {
|
||||
// if (data && (data.paginate || data.count)) {
|
||||
// const { count, paginate, ...response } = data;
|
||||
// this.logger.log(`paginate response from ${className}.${handlerName}`);
|
||||
// const pager = this.formatPage(page, limit, count, request);
|
||||
|
||||
// return {
|
||||
// pager,
|
||||
// ...response,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return data;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
// private formatPage(page: number, limit: number, totalItems: number, request: FastifyRequest): IPageFormat {
|
||||
// const totalPages = Math.ceil(totalItems / limit);
|
||||
// const prevPage = page === 1 ? false : page - 1;
|
||||
// const nextPage = page >= totalPages ? false : page + 1;
|
||||
|
||||
// const formatLink = (pageNum: number | boolean): string | boolean => {
|
||||
// if (!pageNum) return false;
|
||||
// const protocol = request.protocol;
|
||||
// const hostname = request.hostname;
|
||||
// const originalUrl = request.url;
|
||||
// return `${protocol}://${hostname}${originalUrl.split('?')[0]}?page=${pageNum}&limit=${limit}`;
|
||||
// };
|
||||
|
||||
// return {
|
||||
// page,
|
||||
// limit,
|
||||
// totalItems,
|
||||
// totalPages,
|
||||
// prevPage: formatLink(prevPage),
|
||||
// nextPage: formatLink(nextPage),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
constructor() { }
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
||||
// For REST endpoints, wrap the response
|
||||
try {
|
||||
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const statusCode = ctx.statusCode;
|
||||
|
||||
// If statusCode is undefined, skip wrapping
|
||||
if (statusCode === undefined) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
return next.handle().pipe(
|
||||
map(data => {
|
||||
// If data is already wrapped or doesn't need wrapping, return as-is
|
||||
if (data && typeof data === 'object' && 'data' in data && 'success' in data) {
|
||||
console.log('data', data)
|
||||
|
||||
return {
|
||||
data: data,
|
||||
...('nextCursor' in (data as any) && (data as any).nextCursor != null
|
||||
? { nextCursor: (data as any).nextCursor }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a paginated result (has both data and meta properties)
|
||||
if (data && typeof data === 'object' && 'data' in data && 'meta' in data) {
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
};
|
||||
}
|
||||
|
||||
if (data && 'data' in data) {
|
||||
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
...('nextCursor' in data
|
||||
? { nextCursor: (data as any).nextCursor }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// If we can't get HTTP context, return data as-is
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class HTTPLogger implements NestMiddleware {
|
||||
private readonly logger = new Logger(HTTPLogger.name);
|
||||
// use(req: any, res: any, next: (error?: Error | any) => void) {}
|
||||
|
||||
use(req: FastifyRequest, rep: FastifyReply['raw'], next: () => void) {
|
||||
const { ip, method, originalUrl } = req;
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const startAt = process.hrtime();
|
||||
|
||||
rep.on('finish', () => {
|
||||
const { statusCode } = rep;
|
||||
const contentLength = rep.getHeader('content-length') || 0;
|
||||
const dif = process.hrtime(startAt);
|
||||
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
|
||||
this.logger.log(
|
||||
`${method} - ${originalUrl} - ${statusCode} - ${contentLength} - ${userAgent} - ${ip} - ${responseTime.toFixed(2)}ms`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { getSwaggerConfig } from './config/swagger.config';
|
||||
import multipart from '@fastify/multipart';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
|
||||
// 👈 Import the Fastify application type
|
||||
import { type NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
|
||||
import { HttpExceptionFilter } from './core/exceptions/http.exceptions';
|
||||
import { ResponseInterceptor } from './core/interceptors/response.interceptor';
|
||||
// import { PaginationInterceptor } from './core/interceptors/pagination.interceptor';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('APP');
|
||||
|
||||
// 1. Create the application using the Fastify adapter
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(), // Instantiate the FastifyAdapter
|
||||
// Optional: Pass the custom logger (like your 'APP' logger) to NestFactory
|
||||
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] },
|
||||
);
|
||||
|
||||
await app.register(fastifyCookie);
|
||||
|
||||
await app.register(multipart);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
app.useGlobalInterceptors(
|
||||
new ResponseInterceptor(),
|
||||
// , new PaginationInterceptor()
|
||||
);
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
const configService = app.get<ConfigService>(ConfigService);
|
||||
// Ensure the port is handled correctly, use 4000 as fallback if needed
|
||||
const APP_PORT = configService.getOrThrow<number>('APP_PORT') ?? 4000;
|
||||
|
||||
// 2. Swagger Integration Note
|
||||
// Since you are using platform-fastify, you must ensure your getSwaggerConfig
|
||||
// function is configured to use the Fastify platform option if necessary.
|
||||
getSwaggerConfig(app);
|
||||
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 204,
|
||||
});
|
||||
|
||||
// 3. Listen on port using Fastify's listen method
|
||||
// '0.0.0.0' is recommended for Docker/containerized environments
|
||||
await app.listen(APP_PORT, '0.0.0.0', () => {
|
||||
logger.log(`Application is running on: http://localhost:${APP_PORT}`);
|
||||
logger.log(`Swagger documentation: http://localhost:${APP_PORT}/docs`);
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { AdminService } from './providers/admin.service';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Admin } from './entities/admin.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AdminRepository } from './repositories/admin.repository';
|
||||
import { Role } from '../roles/entities/role.entity';
|
||||
import { Permission } from '../roles/entities/permission.entity';
|
||||
import { RolePermission } from '../roles/entities/rolePermission.entity';
|
||||
import { AdminController } from './controllers/admin.controller';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { AdminRole } from './entities/adminRole.entity';
|
||||
import { AdminRoleRepository } from './repositories/admin-role.repository';
|
||||
|
||||
@Module({
|
||||
providers: [AdminService, AdminRepository, AdminRoleRepository],
|
||||
controllers: [AdminController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission, AdminRole]),
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
forwardRef(() => RestaurantsModule),
|
||||
],
|
||||
exports: [AdminService, AdminRepository],
|
||||
})
|
||||
export class AdminModule { }
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Controller, Post, Body, HttpCode, HttpStatus, Get, UseGuards, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { AdminService } from '../providers/admin.service';
|
||||
import { CreateAdminDto } from '../dto/create-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { RestId } from 'src/common/decorators';
|
||||
import { AdminId } from 'src/common/decorators/admin-id.decorator';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('admin')
|
||||
@Controller()
|
||||
export class AdminController {
|
||||
constructor(private readonly adminService: AdminService) { }
|
||||
|
||||
@Get('admin/admins')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
getAll(@RestId() restId: string) {
|
||||
return this.adminService.findAllByRestaurantId(restId);
|
||||
}
|
||||
|
||||
@Get('admin/admins/profile')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
getMe(@AdminId() adminId: string, @RestId() restId: string) {
|
||||
return this.adminService.finAdminById(adminId, restId);
|
||||
}
|
||||
|
||||
@Post('admin/admins')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Create a new admin' })
|
||||
@ApiBody({ type: CreateAdminDto })
|
||||
async create(@Body() dto: CreateAdminDto, @RestId() restId: string) {
|
||||
const admin = await this.adminService.createAdminForMyRestaurant(restId, dto);
|
||||
return admin;
|
||||
}
|
||||
|
||||
@Patch('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Update an admin' })
|
||||
@ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
update(@Param('adminId') adminId: string, @Body() dto: UpdateAdminDto,
|
||||
@RestId() restId: string) {
|
||||
return this.adminService.update(adminId, restId, dto);
|
||||
}
|
||||
|
||||
@Get('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
getById(@Param('adminId') adminId: string, @RestId() restId: string) {
|
||||
return this.adminService.finAdminById(adminId, restId);
|
||||
}
|
||||
|
||||
@Delete('admin/admins/:adminId')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(Permission.MANAGE_ADMINS)
|
||||
@ApiOperation({ summary: 'Delete an admin by ID' })
|
||||
@ApiParam({ name: 'id', description: 'Admin ID' })
|
||||
deleteById(@Param('adminId') adminId: string, @RestId() restId: string) {
|
||||
return this.adminService.remove(adminId, restId);
|
||||
}
|
||||
|
||||
/** Super Admin Endpoints */
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Get('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Get admins for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
getAdminForRestaurant(@Param('restaurantId') restaurantId: string) {
|
||||
return this.adminService.getAdminsForRestaurantBySuperAdmin(restaurantId);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/restaurants/:restaurantId/admins')
|
||||
@ApiOperation({ summary: 'Create admin for a specific restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiBody({ type: CreateMyRestaurantAdminDto })
|
||||
createAdminForRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Body() dto: CreateMyRestaurantAdminDto,
|
||||
): Promise<Admin> {
|
||||
return this.adminService.createAdminForRestaurantBySuperAdmin(restaurantId, dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Delete('super-admin/restaurants/:restaurantId/admins/:adminId')
|
||||
@ApiOperation({ summary: 'Revoke admin role from a restaurant (Super Admin only)' })
|
||||
@ApiParam({ name: 'restaurantId', description: 'Restaurant ID' })
|
||||
@ApiParam({ name: 'adminId', description: 'Admin ID' })
|
||||
revokeAdminFromRestaurant(
|
||||
@Param('restaurantId') restaurantId: string,
|
||||
@Param('adminId') adminId: string,
|
||||
) {
|
||||
return this.adminService.remove(adminId, restaurantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateAdminDto {
|
||||
@ApiProperty({ description: 'Mobile phone number' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: 'First name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Last name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Role id for the admin' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
roleId!: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateMyRestaurantAdminDto {
|
||||
@ApiProperty({ description: 'Mobile phone number' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
phone!: string;
|
||||
|
||||
@ApiProperty({ description: 'First name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
firstName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Last name', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastName?: string;
|
||||
|
||||
@ApiProperty({ description: 'Role id for the admin' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
roleId!: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateAdminDto } from './create-admin.dto';
|
||||
|
||||
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Cascade, Collection, Entity, OneToMany, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { AdminRole } from './adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
firstName?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
|
||||
@OneToMany(() => AdminRole, adminRole => adminRole.admin, {
|
||||
cascade: [Cascade.ALL],
|
||||
orphanRemoval: true,
|
||||
})
|
||||
roles = new Collection<AdminRole>(this);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Entity, Index, ManyToOne, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { Admin } from './admin.entity';
|
||||
|
||||
@Entity({ tableName: 'admin_roles' })
|
||||
@Unique({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin', 'restaurant'] })
|
||||
@Index({ properties: ['admin'] })
|
||||
@Index({ properties: ['restaurant'] })
|
||||
export class AdminRole extends BaseEntity {
|
||||
@ManyToOne(() => Admin)
|
||||
admin!: Admin;
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
role!: Role;
|
||||
|
||||
@ManyToOne(() => Restaurant, { nullable: true })
|
||||
restaurant?: Restaurant;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { wrap } from '@mikro-orm/core';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CreateMyRestaurantAdminDto } from '../dto/create-my-restaurant-admin.dto';
|
||||
import { UpdateAdminDto } from '../dto/update-admin.dto';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
import { AdminRepository } from '../repositories/admin.repository';
|
||||
import { AdminRoleRepository } from '../repositories/admin-role.repository';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly adminRoleRepository: AdminRoleRepository,
|
||||
private readonly em: EntityManager,
|
||||
) { }
|
||||
|
||||
async findByPhone(phone: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
}
|
||||
|
||||
async finAdminById(
|
||||
adminId: string,
|
||||
restId: string,
|
||||
) {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles'], exclude: ['roles'] },
|
||||
);
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin not found');
|
||||
}
|
||||
const adminPlain = wrap(admin).toObject();
|
||||
const adminRole = await this.adminRoleRepository.findOne({ admin: admin, restaurant: { id: restId } });
|
||||
|
||||
return { ...adminPlain, role: adminRole?.role };
|
||||
}
|
||||
|
||||
async findAllByRestaurantId(restId: string) {
|
||||
return this.adminRepository.find({ roles: { restaurant: { id: restId } } }, { populate: ['roles', 'roles.role'] });
|
||||
}
|
||||
|
||||
async createAdminForMyRestaurant(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
|
||||
if (!role) throw new NotFoundException('Role not found');
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
adminRole.role = role;
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async createAdminForRestaurantBySuperAdmin(restId: string, dto: CreateMyRestaurantAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
let admin: Admin | null = null;
|
||||
admin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
});
|
||||
if (!admin) {
|
||||
admin = this.adminRepository.create({
|
||||
phone: normalizedPhone,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.em.persistAndFlush(admin);
|
||||
}
|
||||
const role = await this.em.findOne(Role, { id: roleId });
|
||||
if (!role) throw new NotFoundException('Role* not found' + roleId);
|
||||
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
let adminRole = await this.adminRoleRepository.findOne({
|
||||
admin: admin,
|
||||
restaurant: restaurant,
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
adminRole = this.adminRoleRepository.create({
|
||||
admin: admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
} else {
|
||||
adminRole.role = role;
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(adminRole);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async getAdminsForRestaurantBySuperAdmin(restId: string) {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
|
||||
return this.adminRepository.find({ roles: { restaurant: restaurant } }, { populate: ['roles', 'roles.role'] });
|
||||
}
|
||||
|
||||
async update(adminId: string, restId: string, dto: UpdateAdminDto) {
|
||||
const admin = await this.adminRepository.findOne(
|
||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||
{ populate: ['roles', 'roles.role', 'roles.restaurant'] },
|
||||
);
|
||||
|
||||
if (!admin) {
|
||||
throw new NotFoundException('Admin not found');
|
||||
}
|
||||
|
||||
const { roleId, ...rest } = dto;
|
||||
|
||||
// Update scalar fields (firstName, lastName, phone)
|
||||
if (rest.firstName !== undefined) {
|
||||
admin.firstName = rest.firstName;
|
||||
}
|
||||
if (rest.lastName !== undefined) {
|
||||
admin.lastName = rest.lastName;
|
||||
}
|
||||
if (rest.phone !== undefined && rest.phone !== admin.phone) {
|
||||
const normalizedPhone = normalizePhone(rest.phone);
|
||||
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
if (exists) {
|
||||
throw new ConflictException('This Phone Number is already taken');
|
||||
}
|
||||
admin.phone = normalizedPhone;
|
||||
}
|
||||
|
||||
// Update role if roleId is provided
|
||||
if (roleId) {
|
||||
const role = await this.em.findOne(Role, { id: roleId, isSystem: false });
|
||||
if (!role) {
|
||||
throw new NotFoundException('Role not found');
|
||||
}
|
||||
|
||||
// Find existing AdminRole for this admin and restaurant
|
||||
const existingAdminRole = await this.em.findOne(AdminRole, {
|
||||
admin: { id: adminId },
|
||||
restaurant: { id: restId },
|
||||
});
|
||||
|
||||
if (existingAdminRole) {
|
||||
// Update existing role
|
||||
existingAdminRole.role = role;
|
||||
} else {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||
// Create new AdminRole
|
||||
const newAdminRole = this.em.create(AdminRole, {
|
||||
admin,
|
||||
role,
|
||||
restaurant,
|
||||
});
|
||||
admin.roles.add(newAdminRole);
|
||||
}
|
||||
}
|
||||
|
||||
await this.em.persistAndFlush(admin);
|
||||
return admin;
|
||||
}
|
||||
|
||||
async remove(adminId: string, restId: string): Promise<void> {
|
||||
const adminRole = await this.adminRoleRepository.findOne({ admin: { id: adminId }, restaurant: { id: restId } });
|
||||
if (!adminRole) {
|
||||
throw new NotFoundException('Admin role not found');
|
||||
}
|
||||
return this.em.removeAndFlush(adminRole);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRoleRepository extends EntityRepository<AdminRole> {
|
||||
constructor(
|
||||
readonly em: EntityManager,
|
||||
) {
|
||||
super(em, AdminRole);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { Admin } from '../entities/admin.entity';
|
||||
import { AdminRole } from '../entities/adminRole.entity';
|
||||
import { RestRepository } from '../../restaurants/repositories/rest.repository';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRepository extends EntityRepository<Admin> {
|
||||
constructor(
|
||||
readonly em: EntityManager,
|
||||
private readonly restRepository: RestRepository,
|
||||
) {
|
||||
super(em, Admin);
|
||||
}
|
||||
|
||||
async findByPhoneAndRestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
|
||||
console.log('phone', phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
// First, find the restaurant by slug using the same repository as auth service
|
||||
const restaurant = await this.restRepository.findOne({ slug });
|
||||
if (!restaurant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find AdminRole that matches the admin phone and restaurant
|
||||
const adminRole = await this.em.findOne(
|
||||
AdminRole,
|
||||
{
|
||||
admin: { phone: normalizedPhone },
|
||||
restaurant: { id: restaurant.id },
|
||||
},
|
||||
{ populate: ['admin', 'admin.roles', 'role', 'role.permissions', 'restaurant'] },
|
||||
);
|
||||
|
||||
if (!adminRole || !adminRole.admin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure all roles are populated
|
||||
await adminRole.admin.roles.loadItems();
|
||||
for (const role of adminRole.admin.roles.getItems()) {
|
||||
if (role.role && role.role.permissions && !role.role.permissions.isInitialized()) {
|
||||
await role.role.permissions.loadItems();
|
||||
}
|
||||
}
|
||||
|
||||
return adminRole.admin;
|
||||
}
|
||||
async findAdminsWithPermission(restaurantId: string, permission: string): Promise<Admin[]> {
|
||||
const admins = await this.em.find(
|
||||
Admin,
|
||||
{ roles: { restaurant: { id: restaurantId }, role: { permissions: { name: permission } } } },
|
||||
{ populate: ['roles', 'roles.role', 'roles.role.permissions'] },
|
||||
);
|
||||
return admins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Admin } from '../entities/admin.entity';
|
||||
|
||||
export interface AdminDetailResponse {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
role: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
permissions: string[];
|
||||
restaurant?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Extract permissions - ensure collection is loaded if needed
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { UserModule } from '../users/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { TokensService } from './services/tokens.service';
|
||||
import { RestaurantsModule } from '../restaurants/restaurants.module';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UtilsModule,
|
||||
forwardRef(() => UserModule),
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const expiresIn = configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
|
||||
return {
|
||||
global: true,
|
||||
secret: configService.getOrThrow<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
// Use string format with time unit for explicit expiration (value should be in seconds)
|
||||
expiresIn: `${expiresIn}s`,
|
||||
},
|
||||
};
|
||||
},
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
forwardRef(() => AdminModule),
|
||||
forwardRef(() => RestaurantsModule),
|
||||
MikroOrmModule.forFeature([User, RefreshToken]),
|
||||
forwardRef(() => NotificationsModule),
|
||||
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokensService, AdminAuthGuard],
|
||||
exports: [AdminAuthGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||
import { DirectLoginDto } from '../dto/direct-login.dto';
|
||||
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { VerifyOtpDto } from '../dto/verify-otp.dto';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
|
||||
import { RefreshTokenDto } from '../dto/refresh-token.dto';
|
||||
import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('public/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequest(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, false);
|
||||
}
|
||||
|
||||
@Post('public/auth/otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
otpVerify(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
||||
}
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
||||
@Post('public/auth/refresh')
|
||||
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||
@Post('admin/auth/otp/request')
|
||||
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||
otpRequestAdmin(@Body() dto: RequestOtpDto) {
|
||||
return this.authService.requestOtp(dto, true);
|
||||
}
|
||||
|
||||
@Post('admin/auth/otp/verify')
|
||||
@ApiOperation({ summary: 'Verify OTP code' })
|
||||
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||
otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
|
||||
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp);
|
||||
}
|
||||
|
||||
@RefreshTokenRateLimit()
|
||||
@ApiOperation({ summary: 'refresh the admin access token / refresh token' })
|
||||
@Post('admin/auth/refresh')
|
||||
refreshTokenAdmin(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
//super admin routes
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@Post('super-admin/auth/direct-login')
|
||||
@ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
|
||||
@ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
|
||||
directloginForDsc(@Body() dto: DirectLoginDto) {
|
||||
return this.authService.loginAdminForDsc(dto.phone, dto.slug);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DirectLoginDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: 'Refresh token' })
|
||||
@IsString({ message: 'Refresh token must be a string' })
|
||||
@IsNotEmpty({ message: 'Refresh token is required' })
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RequestOtpDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IsNotEmpty, IsString, Length, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '', description: 'Otp' })
|
||||
@Length(5)
|
||||
otp: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Entity, Enum, Index, Property } from '@mikro-orm/core';
|
||||
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
export enum RefreshTokenType {
|
||||
USER = 'user',
|
||||
ADMIN = 'admin',
|
||||
}
|
||||
|
||||
@Entity({ tableName: 'refreshtokens' })
|
||||
@Index({ properties: ['ownerId', 'restId', 'type'] })
|
||||
@Index({ properties: ['hashedToken'] })
|
||||
@Index({ properties: ['expiresAt'] })
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: 'varchar', length: 255 })
|
||||
hashedToken!: string;
|
||||
|
||||
@Property()
|
||||
ownerId!: string;
|
||||
|
||||
@Property()
|
||||
restId!: string;
|
||||
|
||||
@Enum(() => RefreshTokenType)
|
||||
type!: RefreshTokenType;
|
||||
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
Inject,
|
||||
Logger,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { IAdminTokenPayload } from '../interfaces/IToken-payload';
|
||||
import { PERMISSIONS_KEY } from '../../../common/decorators/permissions.decorator';
|
||||
import { PermissionsService } from 'src/modules/roles/providers/permissions.service';
|
||||
|
||||
export interface AdminAuthRequest extends Request {
|
||||
adminId: string;
|
||||
restId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(AdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly reflector: Reflector,
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<AdminAuthRequest>();
|
||||
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
this.logger.warn('No token provided', {
|
||||
hasAuthHeader: !!request.headers.authorization,
|
||||
authHeader: request.headers.authorization ? 'present' : 'missing',
|
||||
headers: Object.keys(request.headers),
|
||||
});
|
||||
throw new UnauthorizedException('No token provided');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the JWT secret from config
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
|
||||
// Verify token with the secret
|
||||
const payload = await this.jwtService.verifyAsync<IAdminTokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
|
||||
request['adminId'] = payload.adminId;
|
||||
request['restId'] = payload.restId;
|
||||
|
||||
// check if the user has the required permissions
|
||||
const requiredPermissions =
|
||||
this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]) ?? [];
|
||||
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.restId);
|
||||
if (!adminPermission || !Array.isArray(adminPermission)) {
|
||||
this.logger.error('No permissions found', { adminId: payload.adminId, restId: payload.restId });
|
||||
throw new ForbiddenException('No permissions found');
|
||||
}
|
||||
|
||||
const hasPermission = requiredPermissions.every(p => adminPermission.includes(p));
|
||||
|
||||
if (!hasPermission) {
|
||||
this.logger.warn('Insufficient permissions', {
|
||||
adminId: payload.adminId,
|
||||
restId: payload.restId,
|
||||
required: requiredPermissions,
|
||||
has: adminPermission,
|
||||
});
|
||||
throw new ForbiddenException('You are not authorized to access this resource');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof ForbiddenException || err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
const errorStack = err instanceof Error ? err.stack : undefined;
|
||||
this.logger.error('Token verification error in AdminAuthGuard', {
|
||||
error: errorMessage,
|
||||
stack: errorStack,
|
||||
});
|
||||
throw new UnauthorizedException('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const authHeader = request.headers.authorization || request.headers['authorization'];
|
||||
if (!authHeader) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() !== 'bearer' || !token) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||
|
||||
export interface UserAuthRequest extends Request {
|
||||
userId: string;
|
||||
restId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(AuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<UserAuthRequest>();
|
||||
|
||||
try {
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const slug = this.extractSlugFromHeader(request);
|
||||
if (!slug) {
|
||||
throw new UnauthorizedException('Slug is required');
|
||||
}
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
if (!payload.userId || !payload.restId) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
if (payload.slug !== slug) {
|
||||
throw new UnauthorizedException('Invalid slug');
|
||||
}
|
||||
request['userId'] = payload.userId;
|
||||
request['restId'] = payload.restId;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
this.logger.error('error in AuthGuard', err);
|
||||
throw new UnauthorizedException('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
private extractSlugFromHeader(request: Request): string | undefined {
|
||||
// Fastify normalizes headers to lowercase, so check both cases
|
||||
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ITokenPayload } from '../interfaces/IToken-payload';
|
||||
|
||||
export interface UserOptionalAuthRequest extends Request {
|
||||
userId?: string;
|
||||
restId?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OptionalAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(OptionalAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(JwtService)
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<UserOptionalAuthRequest>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const slug = this.extractSlugFromHeader(request);
|
||||
|
||||
// If no token is provided, allow the request without authentication
|
||||
if (!slug) {
|
||||
console.log('No slug provided');
|
||||
this.logger.warn('No slug provided');
|
||||
return false;
|
||||
}
|
||||
if (!token) {
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to verify the token if it exists
|
||||
try {
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
|
||||
// Validate payload structure
|
||||
if (!payload.userId || !payload.restId) {
|
||||
this.logger.warn('Invalid token payload structure', payload);
|
||||
// Allow request but don't set user info
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate slug if provided
|
||||
if (slug && payload.slug !== slug) {
|
||||
this.logger.warn('Token slug does not match header slug', {
|
||||
tokenSlug: payload.slug,
|
||||
headerSlug: slug,
|
||||
});
|
||||
// Allow request but don't set user info
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Token is valid, set user info
|
||||
request['userId'] = payload.userId;
|
||||
request['restId'] = payload.restId;
|
||||
request['slug'] = slug;
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Token is invalid or expired, but allow the request anyway
|
||||
this.logger.debug('Token verification failed in OptionalAuthGuard', {
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
// Set restId from slug
|
||||
|
||||
request['slug'] = slug;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
|
||||
private extractSlugFromHeader(request: Request): string | undefined {
|
||||
// Fastify normalizes headers to lowercase, so check both cases
|
||||
const slug = (request.headers['x-slug'] || request.headers['X-Slug']) as string;
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class SuperAdminAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(SuperAdminAuthGuard.name);
|
||||
|
||||
constructor(
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
|
||||
try {
|
||||
const credentials = this.extractBasicAuthCredentials(request);
|
||||
if (!credentials) {
|
||||
throw new UnauthorizedException('Basic authentication required');
|
||||
}
|
||||
|
||||
const { username, password } = credentials;
|
||||
const expectedUsername = this.configService.get<string>('SUPER_ADMIN_USERNAME') ?? 'danak@dsc.com';
|
||||
const expectedPassword = this.configService.get<string>('SUPER_ADMIN_PASSWORD') ?? 'DsCdAnAk?@ABC';
|
||||
if (username !== expectedUsername || password !== expectedPassword) {
|
||||
this.logger.warn(`Invalid super admin credentials attempt for username: ${username}`);
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedException) {
|
||||
throw err;
|
||||
}
|
||||
this.logger.error('error in SuperAdminAuthGuard', err);
|
||||
throw new UnauthorizedException('Authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private extractBasicAuthCredentials(request: Request): { username: string; password: string } | null {
|
||||
const authHeader = request.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [type, encoded] = authHeader.split(' ');
|
||||
if (type?.toLowerCase() !== 'basic' || !encoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
|
||||
const [username, password] = decoded.split(':');
|
||||
|
||||
if (!username || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { username, password };
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to decode Basic Auth credentials', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export interface ITokenPayload {
|
||||
userId: string;
|
||||
restId: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface IAdminTokenPayload {
|
||||
adminId: string;
|
||||
restId: string;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { SmsService } from '../../notifications/services/sms.service';
|
||||
import { UserService } from '../../users/providers/user.service';
|
||||
import { randomInt } from 'crypto';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository';
|
||||
import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
|
||||
import { UserLoginTransformer } from '../transformers/user-login.transformer';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { normalizePhone } from '../../utils/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
|
||||
private OTP_EXPIRATION_TIME: number;
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly userService: UserService,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly adminRepository: AdminRepository,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240;
|
||||
}
|
||||
|
||||
private userOtpKey(restaurantSlug: string, phone: string) {
|
||||
return `otp:${restaurantSlug}:${phone}`;
|
||||
}
|
||||
|
||||
private adminOtpKey(restaurantSlug: string, phone: string) {
|
||||
return `otp-admin:${restaurantSlug}:${phone}`;
|
||||
}
|
||||
|
||||
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
const { phone, slug } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
if (isAdmin) {
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
const code = this.generateOtpCode();
|
||||
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone);
|
||||
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||
|
||||
await this.smsService.sendotp(normalizedPhone, code);
|
||||
|
||||
return { code: null };
|
||||
}
|
||||
|
||||
async verifyOtp(phone: string, slug: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.userOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const user = await this.userService.findOrCreateByPhone(normalizedPhone);
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug);
|
||||
|
||||
const userResponse = UserLoginTransformer.transform(user, rest);
|
||||
|
||||
return { tokens, user: userResponse };
|
||||
}
|
||||
|
||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const key = this.adminOtpKey(slug, normalizedPhone);
|
||||
const cachedCode = await this.cacheService.get(key);
|
||||
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
await this.cacheService.del(key);
|
||||
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
||||
|
||||
return { tokens, admin: adminResponse };
|
||||
}
|
||||
/**
|
||||
*
|
||||
to use for login directly from DSC
|
||||
*/
|
||||
async loginAdminForDsc(phone: string, slug: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
}
|
||||
|
||||
const rest = await this.restRepository.findOne({ slug });
|
||||
|
||||
if (!rest) {
|
||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
|
||||
|
||||
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
||||
|
||||
return { tokens, admin: adminResponse };
|
||||
}
|
||||
|
||||
private generateOtpCode(): string {
|
||||
const code = randomInt(10000, 100000);
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
refreshToken(oldRefreshToken: string) {
|
||||
return this.tokensService.refreshToken(oldRefreshToken);
|
||||
}
|
||||
}
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { LockMode } from '@mikro-orm/core';
|
||||
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { AuthMessage } from '../../../common/enums/message.enum';
|
||||
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
|
||||
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
private readonly logger = new Logger(TokensService.name);
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async generateAccessAndRefreshToken(
|
||||
ownerId: string,
|
||||
restaurantId: string,
|
||||
isAdmin: boolean,
|
||||
slug: string,
|
||||
em?: EntityManager,
|
||||
) {
|
||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||
const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
|
||||
|
||||
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
|
||||
? { adminId: ownerId, restId: restaurantId }
|
||||
: { userId: ownerId, restId: restaurantId, slug };
|
||||
|
||||
const accessToken = await this.generateAccessToken(payload, accessExpire);
|
||||
const refreshToken = this.generateRefreshToken();
|
||||
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER;
|
||||
|
||||
// Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush
|
||||
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type, em);
|
||||
|
||||
return {
|
||||
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() },
|
||||
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
|
||||
};
|
||||
}
|
||||
|
||||
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expiresIn: number) {
|
||||
// Ensure expiresIn is passed as a string with time unit for reliability
|
||||
// JWT library accepts: number (seconds) or string with unit (e.g., "3600s", "1h")
|
||||
// Using string format is more explicit and prevents unit confusion
|
||||
return this.jwtService.signAsync(payload, { expiresIn: `${expiresIn}s` });
|
||||
}
|
||||
|
||||
async storeRefreshToken(
|
||||
ownerId: string,
|
||||
restId: string,
|
||||
refreshToken: string,
|
||||
type: RefreshTokenType,
|
||||
em?: EntityManager,
|
||||
) {
|
||||
const entityManager = em || this.em;
|
||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
|
||||
|
||||
const hashedToken = this.hashToken(refreshToken);
|
||||
|
||||
const token = entityManager.create(RefreshToken, {
|
||||
hashedToken,
|
||||
ownerId,
|
||||
restId,
|
||||
type,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
if (em) {
|
||||
// Within transaction, just persist (flush will be called by transaction)
|
||||
entityManager.persist(token);
|
||||
} else {
|
||||
// Outside transaction, persist and flush immediately
|
||||
await entityManager.persistAndFlush(token);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
const hashedToken = this.hashToken(oldRefreshToken);
|
||||
|
||||
// Use transaction to ensure atomicity and prevent race conditions
|
||||
return this.em.transactional(async em => {
|
||||
// Lock the token row to prevent concurrent refresh attempts
|
||||
// Using pessimistic write lock (FOR UPDATE) to prevent race conditions
|
||||
const token = await em.findOne(RefreshToken, { hashedToken }, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||
em.remove(token);
|
||||
await em.flush();
|
||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
// Store token data before removal
|
||||
const ownerId = token.ownerId;
|
||||
const restId = token.restId;
|
||||
const isAdmin = token.type === RefreshTokenType.ADMIN;
|
||||
|
||||
// Verify restaurant still exists
|
||||
const restaurant = await em.findOne(Restaurant, { id: restId });
|
||||
if (!restaurant) {
|
||||
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate new tokens first (before removing old token)
|
||||
// Pass the transaction EntityManager to ensure operations are within the transaction
|
||||
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
|
||||
|
||||
// Remove old token only after new token is created
|
||||
em.remove(token);
|
||||
|
||||
// Persist changes atomically
|
||||
await em.flush();
|
||||
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate new tokens after refresh', error);
|
||||
// Transaction will rollback automatically, preserving the old token
|
||||
throw new UnauthorizedException('Failed to refresh token');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken() {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { Admin } from '../../admin/entities/admin.entity';
|
||||
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
|
||||
export interface AdminLoginResponse {
|
||||
id: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
restaurant?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AdminLoginTransformer {
|
||||
static async transform(admin: Admin, restaurant?: Restaurant): Promise<AdminLoginResponse> {
|
||||
// Find the AdminRole that matches the restaurant (or get the first one)
|
||||
let adminRole = admin.roles.getItems().find(r => (restaurant ? r.restaurant?.id === restaurant.id : true));
|
||||
|
||||
// If no match found, get the first one
|
||||
if (!adminRole && admin.roles.getItems().length > 0) {
|
||||
adminRole = admin.roles.getItems()[0];
|
||||
}
|
||||
|
||||
if (!adminRole) {
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: '',
|
||||
permissions: [],
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the role from AdminRole
|
||||
const role = adminRole.role;
|
||||
if (!role) {
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: '',
|
||||
permissions: [],
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract permissions - ensure collection is loaded if needed
|
||||
let permissions: string[] = [];
|
||||
if (role.permissions) {
|
||||
if (role.permissions.isInitialized()) {
|
||||
permissions = role.permissions.getItems().map(p => p.name);
|
||||
} else {
|
||||
await role.permissions.loadItems();
|
||||
permissions = role.permissions.getItems().map(p => p.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: admin.id,
|
||||
firstName: admin.firstName,
|
||||
lastName: admin.lastName,
|
||||
phone: admin.phone,
|
||||
role: role.name,
|
||||
permissions,
|
||||
restaurant: restaurant
|
||||
? {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { User } from '../../users/entities/user.entity';
|
||||
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
|
||||
export interface UserLoginResponse {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
phone: string;
|
||||
|
||||
isActive?: boolean;
|
||||
restaurant: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class UserLoginTransformer {
|
||||
static transform(user: User, restaurant: Restaurant): UserLoginResponse {
|
||||
return {
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
phone: user.phone,
|
||||
isActive: user.isActive,
|
||||
restaurant: {
|
||||
id: restaurant.id,
|
||||
name: restaurant.name,
|
||||
slug: restaurant.slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { CartService } from './providers/cart.service';
|
||||
import { CartController } from './controllers/cart.controller';
|
||||
import { Food } from '../foods/entities/food.entity';
|
||||
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||
import { PaymentMethod } from '../payments/entities/payment-method.entity';
|
||||
import { UserAddress } from '../users/entities/user-address.entity';
|
||||
import { User } from '../users/entities/user.entity';
|
||||
import { Delivery } from '../delivery/entities/delivery.entity';
|
||||
import { Order } from '../orders/entities/order.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { UserModule } from '../users/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { CouponModule } from '../coupons/coupon.module';
|
||||
import { CartRepository } from './repositories/cart.repository';
|
||||
import { CartValidationService } from './providers/cart-validation.service';
|
||||
import { CartCalculationService } from './providers/cart-calculation.service';
|
||||
import { CartItemService } from './providers/cart-item.service';
|
||||
import { PointTransaction } from '../users/entities/point-transaction.entity';
|
||||
import { WalletTransaction } from '../users/entities/wallet-transaction.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([
|
||||
Food,
|
||||
Restaurant,
|
||||
PaymentMethod,
|
||||
UserAddress,
|
||||
User,
|
||||
PointTransaction,
|
||||
WalletTransaction,
|
||||
Delivery,
|
||||
Order,
|
||||
]),
|
||||
AuthModule,
|
||||
UserModule,
|
||||
JwtModule,
|
||||
UtilsModule,
|
||||
CouponModule,
|
||||
],
|
||||
controllers: [CartController],
|
||||
providers: [
|
||||
CartService,
|
||||
CartRepository,
|
||||
CartValidationService,
|
||||
CartCalculationService,
|
||||
CartItemService,
|
||||
],
|
||||
exports: [CartService],
|
||||
})
|
||||
export class CartModule { }
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Controller, Get, Post, Body, Patch, Delete, UseGuards, Param } from '@nestjs/common';
|
||||
import { CartService } from '../providers/cart.service';
|
||||
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
||||
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiParam, ApiHeader } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '../../auth/guards/auth.guard';
|
||||
import { UserId } from 'src/common/decorators/user-id.decorator';
|
||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants/index';
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('cart')
|
||||
@Controller('public/cart')
|
||||
export class CartController {
|
||||
constructor(private readonly cartService: CartService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get cart for current user and restaurant' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
async findOne(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||
return this.cartService.getOrCreateCart(userId, restaurantId);
|
||||
}
|
||||
|
||||
@Post('items/:foodId/increment')
|
||||
@ApiOperation({ summary: 'Increment item quantity in cart by 1' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'foodId', description: 'Food ID to increment in cart' })
|
||||
async incrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
|
||||
return this.cartService.incrementItem(userId, restaurantId, foodId, 1);
|
||||
}
|
||||
|
||||
@Post('items/:foodId/decrement')
|
||||
@ApiOperation({ summary: 'Decrement item quantity in cart by 1 (removes item if quantity reaches 0)' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'foodId', description: 'Food ID to decrement in cart' })
|
||||
async decrementItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
|
||||
return this.cartService.decrementItem(userId, restaurantId, foodId);
|
||||
}
|
||||
|
||||
@Post('items/bulk')
|
||||
@ApiOperation({ summary: 'Bulk add items to cart (increments quantity if items exist)' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBody({ type: BulkAddItemsToCartDto })
|
||||
bulkAddItems(
|
||||
@UserId() userId: string,
|
||||
@RestId() restaurantId: string,
|
||||
@Body() bulkAddItemsDto: BulkAddItemsToCartDto,
|
||||
) {
|
||||
return this.cartService.bulkAddItems(userId, restaurantId, bulkAddItemsDto);
|
||||
}
|
||||
|
||||
@Delete('items/:foodId')
|
||||
@ApiOperation({ summary: 'Remove item from cart' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'foodId', description: 'Food ID in the cart' })
|
||||
async removeItem(@UserId() userId: string, @RestId() restaurantId: string, @Param('foodId') foodId: string) {
|
||||
return this.cartService.removeItem(userId, restaurantId, foodId);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@ApiOperation({ summary: 'Clear entire cart' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
async clearCart(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||
return this.cartService.clearCart(userId, restaurantId);
|
||||
}
|
||||
|
||||
@Post('apply-coupon')
|
||||
@ApiOperation({ summary: 'Apply coupon to cart' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBody({ type: ApplyCouponDto })
|
||||
async applyCoupon(@UserId() userId: string, @RestId() restaurantId: string, @Body() applyCouponDto: ApplyCouponDto) {
|
||||
return this.cartService.applyCoupon(userId, restaurantId, applyCouponDto);
|
||||
}
|
||||
|
||||
@Delete('coupon')
|
||||
@ApiOperation({ summary: 'Remove coupon from cart' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
async removeCoupon(@UserId() userId: string, @RestId() restaurantId: string) {
|
||||
return this.cartService.removeCoupon(userId, restaurantId);
|
||||
}
|
||||
|
||||
|
||||
@Patch('all')
|
||||
@ApiOperation({ summary: 'Set all cart params' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiBody({ type: SetAllCartParmsDto })
|
||||
setAllCartParams(@UserId() userId: string, @RestId() restaurantId: string, @Body() dto: SetAllCartParmsDto) {
|
||||
return this.cartService.setAllCartParams(userId, restaurantId, dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ApplyCouponDto {
|
||||
@ApiProperty({ description: 'Coupon code', example: 'DISCOUNT10' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
code!: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, Min, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class AddItemToCartDto {
|
||||
@ApiProperty({ description: 'Quantity of the food item', example: 1, minimum: 1 })
|
||||
@IsNotEmpty()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
quantity!: number;
|
||||
|
||||
@ApiProperty({ description: 'Food ID' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
foodId!: string;
|
||||
}
|
||||
|
||||
export class BulkAddItemsToCartDto {
|
||||
@ApiProperty({
|
||||
description: 'Array of items to add to cart',
|
||||
type: [AddItemToCartDto],
|
||||
example: [
|
||||
{ foodId: 'food-123', quantity: 2 },
|
||||
{ foodId: 'food-456', quantity: 1 },
|
||||
],
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1, { message: 'At least one item is required' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AddItemToCartDto)
|
||||
items!: AddItemToCartDto[];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, IsArray, ValidateNested, IsInt, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CartItemDto {
|
||||
@ApiProperty({ description: 'Food ID' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
foodId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Quantity of the food item', example: 2, minimum: 1 })
|
||||
@IsNotEmpty()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
quantity!: number;
|
||||
}
|
||||
|
||||
export class CreateCartDto {
|
||||
@ApiProperty({
|
||||
description: 'List of cart items',
|
||||
type: [CartItemDto],
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CartItemDto)
|
||||
items!: CartItemDto[];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { SetCarDeliveryDto } from './set-car-delivery.dto';
|
||||
|
||||
|
||||
export class SetAllCartParmsDto {
|
||||
@ApiProperty({
|
||||
description: 'Cart description or notes',
|
||||
required: false,
|
||||
example: 'Please deliver to the back door',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: 'Delivery method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
deliveryMethodId!: string;
|
||||
|
||||
@ApiProperty({ description: 'User address ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
addressId?: string;
|
||||
|
||||
@ApiProperty({ description: 'Payment method ID', example: '01ARZ3NDEKTSV4RRFFQ69G5FAV' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
paymentMethodId!: string;
|
||||
|
||||
@ApiProperty({ description: 'Table number for dine-in orders', example: '5', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
tableNumber?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
carAddress?: SetCarDeliveryDto;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class SetCarDeliveryDto {
|
||||
@ApiProperty({ description: 'Car model', example: 'Toyota Camry' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
carModel!: string;
|
||||
|
||||
@ApiProperty({ description: 'Car color', example: 'White' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
carColor!: string;
|
||||
|
||||
@ApiProperty({ description: 'License plate number', example: '12ABC345' })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
plateNumber!: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PartialType, ApiProperty } from '@nestjs/swagger';
|
||||
import { CreateCartDto, CartItemDto } from './create-cart.dto';
|
||||
|
||||
export class UpdateCartItemDto extends PartialType(CartItemDto) {}
|
||||
|
||||
export class UpdateCartDto {
|
||||
@ApiProperty({ description: 'List of cart items to update', type: [UpdateCartItemDto], required: false })
|
||||
items?: UpdateCartItemDto[];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { OrderCouponDetail, OrderUserAddress, OrderCarAddress } from 'src/modules/orders/interface/order.interface';
|
||||
|
||||
export interface CartItem {
|
||||
foodId: string;
|
||||
foodTitle?: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
discount: number;
|
||||
totalPrice: number;
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
userId: string;
|
||||
restaurantId: string;
|
||||
restaurantName?: string;
|
||||
|
||||
items: CartItem[];
|
||||
coupon?: OrderCouponDetail;
|
||||
paymentMethodId?: string;
|
||||
deliveryMethodId?: string;
|
||||
description?: string;
|
||||
tableNumber?: string;
|
||||
|
||||
deliveryFee: number;
|
||||
subTotal: number;
|
||||
tax: number;
|
||||
couponDiscount: number;
|
||||
itemsDiscount: number;
|
||||
totalDiscount: number;
|
||||
total: number;
|
||||
|
||||
carAddress?: OrderCarAddress | null;
|
||||
userAddress?: OrderUserAddress | null;
|
||||
|
||||
totalItems: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { Delivery } from '../../delivery/entities/delivery.entity';
|
||||
import { DeliveryFeeTypeEnum } from '../../delivery/interface/delivery';
|
||||
import { CouponType } from '../../coupons/interface/coupon';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
import { CouponService } from '../../coupons/providers/coupon.service';
|
||||
import { GeographicUtils } from '../utils/geographic.utils';
|
||||
|
||||
@Injectable()
|
||||
export class CartCalculationService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly couponService: CouponService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Calculate total price for a cart item
|
||||
*/
|
||||
calculateItemTotalPrice(unitPrice: number, unitDiscount: number, quantity: number): number {
|
||||
const safeUnitPrice = Number(unitPrice) || 0;
|
||||
const safeUnitDiscount = Math.min(Number(unitDiscount) || 0, safeUnitPrice);
|
||||
const itemTotalPrice = safeUnitPrice * quantity;
|
||||
const itemTotalDiscount = safeUnitDiscount * quantity;
|
||||
return itemTotalPrice - itemTotalDiscount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate items totals (subtotal, items discount, total items)
|
||||
*/
|
||||
calculateItemsTotals(cart: Cart): { subTotal: number; itemsDiscount: number; totalItems: number } {
|
||||
let subTotal = 0;
|
||||
let itemsDiscount = 0;
|
||||
let totalItems = 0;
|
||||
|
||||
for (const item of cart.items) {
|
||||
const unitPrice = Number(item.price) || 0;
|
||||
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
|
||||
const itemPrice = unitPrice * item.quantity;
|
||||
const itemDiscount = unitDiscount * item.quantity;
|
||||
subTotal += itemPrice;
|
||||
itemsDiscount += itemDiscount;
|
||||
totalItems += item.quantity;
|
||||
item.totalPrice = itemPrice - itemDiscount;
|
||||
}
|
||||
|
||||
return { subTotal, itemsDiscount, totalItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate coupon discount
|
||||
*/
|
||||
async calculateCouponDiscount(cart: Cart, subTotal: number, itemsDiscount: number): Promise<number> {
|
||||
if (!cart.coupon) return 0;
|
||||
|
||||
const coupon = await this.getCouponRestrictionsOrClear(cart);
|
||||
if (!cart.coupon || !coupon) return 0;
|
||||
|
||||
const hasCategoryRestriction = (coupon.foodCategories?.length ?? 0) > 0;
|
||||
const hasFoodRestriction = (coupon.foods?.length ?? 0) > 0;
|
||||
|
||||
let eligibleItemsTotal = subTotal;
|
||||
let eligibleItemsDiscount = itemsDiscount;
|
||||
|
||||
if (hasCategoryRestriction || hasFoodRestriction) {
|
||||
eligibleItemsTotal = 0;
|
||||
eligibleItemsDiscount = 0;
|
||||
|
||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||
|
||||
for (const item of cart.items) {
|
||||
const food = foodMap.get(item.foodId);
|
||||
if (!food) continue;
|
||||
|
||||
const matchesCategory =
|
||||
hasCategoryRestriction && food.category && (coupon.foodCategories ?? []).includes(food.category.id);
|
||||
const matchesFood = hasFoodRestriction && (coupon.foods ?? []).includes(food.id);
|
||||
|
||||
if (matchesCategory || matchesFood) {
|
||||
const unitPrice = Number(item.price) || 0;
|
||||
const unitDiscount = Math.min(Number(item.discount) || 0, unitPrice);
|
||||
eligibleItemsTotal += unitPrice * item.quantity;
|
||||
eligibleItemsDiscount += unitDiscount * item.quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const priceAfterItemDiscount = Math.max(0, eligibleItemsTotal - eligibleItemsDiscount);
|
||||
|
||||
if (cart.coupon.type === CouponType.PERCENTAGE) {
|
||||
let discount = (priceAfterItemDiscount * cart.coupon.value) / 100;
|
||||
if (cart.coupon.maxDiscount && discount > cart.coupon.maxDiscount) {
|
||||
discount = cart.coupon.maxDiscount;
|
||||
}
|
||||
return discount;
|
||||
}
|
||||
|
||||
return Math.min(cart.coupon.value, priceAfterItemDiscount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tax
|
||||
*/
|
||||
async calculateTax(restaurantId: string, amountAfterDiscounts: number): Promise<number> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
const vat = restaurant?.vat ? Number(restaurant.vat) : 0;
|
||||
if (!vat || vat <= 0) return 0;
|
||||
return (Math.max(0, amountAfterDiscounts) * vat) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate delivery fee
|
||||
*/
|
||||
async calculateDeliveryFee(cart: Cart): Promise<number> {
|
||||
const deliveryMethodId = cart.deliveryMethodId;
|
||||
if (!deliveryMethodId) return 0;
|
||||
|
||||
const deliveryMethod = await this.em.findOne(Delivery, { id: deliveryMethodId }, { populate: ['restaurant'] });
|
||||
if (!deliveryMethod || !deliveryMethod.enabled) return 0;
|
||||
|
||||
// If not distance based, return fixed delivery fee
|
||||
if (deliveryMethod.deliveryFeeType !== DeliveryFeeTypeEnum.DISTANCE_BASED) {
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
}
|
||||
|
||||
// For distance based calculation we need restaurant and user coordinates
|
||||
const restaurant = (deliveryMethod as any).restaurant as Restaurant | undefined;
|
||||
const userAddr = cart.userAddress;
|
||||
|
||||
if (!restaurant || restaurant.latitude == null || restaurant.longitude == null) {
|
||||
// fallback to configured fixed fee when coordinates are missing
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
}
|
||||
|
||||
if (!userAddr || userAddr.latitude == null || userAddr.longitude == null) {
|
||||
return Number(deliveryMethod.deliveryFee) || 0;
|
||||
}
|
||||
|
||||
const restLat = Number(restaurant.latitude);
|
||||
const restLng = Number(restaurant.longitude);
|
||||
const userLat = Number(userAddr.latitude);
|
||||
const userLng = Number(userAddr.longitude);
|
||||
|
||||
const distanceKm = GeographicUtils.getDistanceKmRounded(restLat, restLng, userLat, userLng);
|
||||
|
||||
// Try to read either possible property names (legacy vs new)
|
||||
const perKm = Number(deliveryMethod.perKilometerFee);
|
||||
const minFee = Number(deliveryMethod.distanceBasedMinCost);
|
||||
|
||||
let fee = 0;
|
||||
if (perKm <= 0) {
|
||||
fee = Number(deliveryMethod.deliveryFee) || 0;
|
||||
} else {
|
||||
fee = distanceKm * perKm;
|
||||
}
|
||||
|
||||
if (minFee > 0 && fee < minFee) fee = minFee;
|
||||
|
||||
return Math.max(0, Number(fee));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate cart totals (including coupon discount and tax)
|
||||
*/
|
||||
async recalculateCartTotals(cart: Cart): Promise<void> {
|
||||
const { subTotal, itemsDiscount, totalItems } = this.calculateItemsTotals(cart);
|
||||
cart.subTotal = subTotal;
|
||||
cart.itemsDiscount = itemsDiscount;
|
||||
|
||||
const couponDiscount = await this.calculateCouponDiscount(cart, subTotal, itemsDiscount);
|
||||
cart.couponDiscount = couponDiscount;
|
||||
cart.totalDiscount = couponDiscount + itemsDiscount;
|
||||
|
||||
cart.tax = await this.calculateTax(cart.restaurantId, Math.max(0, subTotal - cart.totalDiscount));
|
||||
cart.deliveryFee = await this.calculateDeliveryFee(cart);
|
||||
|
||||
// total = subtotal – totalDiscount + tax + deliveryFee
|
||||
cart.total = Math.max(0, subTotal - cart.totalDiscount) + cart.tax + cart.deliveryFee;
|
||||
cart.totalItems = totalItems;
|
||||
cart.updatedAt = this.nowIso();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coupon restrictions or clear if invalid
|
||||
*/
|
||||
private async getCouponRestrictionsOrClear(cart: Cart): Promise<{
|
||||
isActive?: boolean;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
foodCategories?: string[];
|
||||
foods?: string[];
|
||||
} | null> {
|
||||
if (!cart.coupon) return null;
|
||||
|
||||
try {
|
||||
const c = await this.couponService.findById(cart.coupon.couponId);
|
||||
const now = new Date();
|
||||
if (c.isActive === false) {
|
||||
cart.coupon = undefined;
|
||||
return null;
|
||||
}
|
||||
if (c.startDate && now < c.startDate) {
|
||||
cart.coupon = undefined;
|
||||
return null;
|
||||
}
|
||||
if (c.endDate && now > c.endDate) {
|
||||
cart.coupon = undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
isActive: c.isActive,
|
||||
startDate: c.startDate,
|
||||
endDate: c.endDate,
|
||||
foodCategories: c.foodCategories,
|
||||
foods: c.foods,
|
||||
};
|
||||
} catch {
|
||||
cart.coupon = undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foods in cart with categories
|
||||
*/
|
||||
private async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Food>> {
|
||||
const foodIds = cart.items.map(item => item.foodId);
|
||||
if (foodIds.length === 0) return new Map();
|
||||
|
||||
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
|
||||
return new Map(foods.map(f => [f.id, f]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current ISO timestamp
|
||||
*/
|
||||
private nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
import { Cart, CartItem } from '../interfaces/cart.interface';
|
||||
import { CartValidationService } from './cart-validation.service';
|
||||
import { CartCalculationService } from './cart-calculation.service';
|
||||
import { CartMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CartItemService {
|
||||
constructor(
|
||||
private readonly validationService: CartValidationService,
|
||||
private readonly calculationService: CartCalculationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new cart item from food
|
||||
*/
|
||||
createCartItem(food: Food, quantity: number): CartItem {
|
||||
const itemPrice = food.price || 0;
|
||||
const itemDiscount = food.discount || 0;
|
||||
|
||||
return {
|
||||
foodId: food.id,
|
||||
foodTitle: food.title,
|
||||
quantity,
|
||||
price: itemPrice,
|
||||
discount: itemDiscount,
|
||||
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cart item from food (updating existing item if provided)
|
||||
*/
|
||||
buildCartItemFromFood(food: Food, quantity: number, previous?: CartItem): CartItem {
|
||||
const itemPrice = food.price || 0;
|
||||
const itemDiscount = food.discount || 0;
|
||||
return {
|
||||
...(previous ?? ({} as CartItem)),
|
||||
foodId: food.id,
|
||||
foodTitle: food.title,
|
||||
quantity,
|
||||
price: itemPrice,
|
||||
discount: itemDiscount,
|
||||
totalPrice: this.calculationService.calculateItemTotalPrice(itemPrice, itemDiscount, quantity),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item index in cart
|
||||
*/
|
||||
getItemIndex(cart: Cart, foodId: string): number {
|
||||
return cart.items.findIndex(item => item.foodId === foodId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or increment item in cart
|
||||
*/
|
||||
async addOrIncrementItem(cart: Cart, foodId: string, restaurantId: string, quantityToAdd: number): Promise<void> {
|
||||
const food = await this.validationService.validateAndGetFood(foodId, restaurantId, quantityToAdd);
|
||||
|
||||
// Validate meal type compatibility
|
||||
this.validationService.assertMealTypeCompatibility(food);
|
||||
|
||||
// Validate weekday compatibility
|
||||
this.validationService.assertWeekdayCompatibility(food);
|
||||
|
||||
const index = this.getItemIndex(cart, food.id);
|
||||
|
||||
if (index < 0) {
|
||||
cart.items.push(this.createCartItem(food, quantityToAdd));
|
||||
return;
|
||||
}
|
||||
|
||||
const existingItem = cart.items[index];
|
||||
const newQuantity = existingItem.quantity + quantityToAdd;
|
||||
this.validationService.validateStock(food, newQuantity);
|
||||
cart.items[index] = this.buildCartItemFromFood(food, newQuantity, existingItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from cart
|
||||
*/
|
||||
removeItemOrFail(cart: Cart, foodId: string): void {
|
||||
const itemIndex = this.getItemIndex(cart, foodId);
|
||||
if (itemIndex < 0) {
|
||||
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
||||
}
|
||||
cart.items.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement item quantity or remove if quantity reaches 0
|
||||
*/
|
||||
async decrementOrRemoveItem(cart: Cart, foodId: string): Promise<void> {
|
||||
const itemIndex = this.getItemIndex(cart, foodId);
|
||||
if (itemIndex < 0) {
|
||||
throw new NotFoundException(CartMessage.ITEM_NOT_FOUND);
|
||||
}
|
||||
|
||||
const existingItem = cart.items[itemIndex];
|
||||
const newQuantity = existingItem.quantity - 1;
|
||||
|
||||
if (newQuantity <= 0) {
|
||||
cart.items.splice(itemIndex, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
const food = await this.validationService.getFoodOrFail(foodId);
|
||||
cart.items[itemIndex] = this.buildCartItemFromFood(food, newQuantity, existingItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Food } from '../../foods/entities/food.entity';
|
||||
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
|
||||
import { UserAddress } from '../../users/entities/user-address.entity';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { PaymentMethod } from '../../payments/entities/payment-method.entity';
|
||||
import { Delivery } from '../../delivery/entities/delivery.entity';
|
||||
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
|
||||
import { PointTransaction } from '../../users/entities/point-transaction.entity';
|
||||
import { Order } from '../../orders/entities/order.entity';
|
||||
import { OrderStatus } from '../../orders/interface/order.interface';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
import { GeographicUtils } from '../utils/geographic.utils';
|
||||
import { MealType } from '../../foods/interface/food.interface';
|
||||
import { CartMessage } from 'src/common/enums/message.enum';
|
||||
import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CartValidationService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Validate food exists, belongs to restaurant, has inventory, and check stock
|
||||
*/
|
||||
async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise<Food> {
|
||||
const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] });
|
||||
if (!food) {
|
||||
throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (food.restaurant.id !== restaurantId) {
|
||||
throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT);
|
||||
}
|
||||
|
||||
if (!food.inventory) {
|
||||
throw new BadRequestException(CartMessage.FOOD_NO_INVENTORY);
|
||||
}
|
||||
|
||||
this.validateStock(food, quantity);
|
||||
return food;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate stock availability for food
|
||||
*/
|
||||
validateStock(food: Food, quantity: number): void {
|
||||
const availableStock = food.inventory!.availableStock;
|
||||
if (availableStock < quantity) {
|
||||
throw new BadRequestException(CartMessage.INSUFFICIENT_STOCK + food.title);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user or throw if not found
|
||||
*/
|
||||
async getUserOrFail(userId: string): Promise<User> {
|
||||
const user = await this.em.findOne(User, { id: userId });
|
||||
if (!user) {
|
||||
throw new NotFoundException(CartMessage.USER_NOT_FOUND);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user address or throw if not found
|
||||
*/
|
||||
async getUserAddressOrFail(addressId: string): Promise<UserAddress> {
|
||||
const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] });
|
||||
if (!address) {
|
||||
throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get food or throw if not found
|
||||
*/
|
||||
async getFoodOrFail(foodId: string): Promise<Food> {
|
||||
const food = await this.em.findOne(Food, { id: foodId });
|
||||
if (!food) {
|
||||
throw new NotFoundException(CartMessage.FOOD_NOT_FOUND);
|
||||
}
|
||||
return food;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restaurant or throw if not found
|
||||
*/
|
||||
async getRestaurantOrFail(restaurantId: string): Promise<Restaurant> {
|
||||
const restaurant = await this.em.findOne(Restaurant, { id: restaurantId });
|
||||
if (!restaurant) {
|
||||
throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND);
|
||||
}
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate address belongs to user
|
||||
*/
|
||||
validateAddressOwnership(address: UserAddress, userId: string): void {
|
||||
if (address.user.id !== userId) {
|
||||
throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert address is inside restaurant service area
|
||||
*/
|
||||
async assertAddressInsideServiceArea(
|
||||
restaurantId: string,
|
||||
latitude?: number | null,
|
||||
longitude?: number | null,
|
||||
): Promise<void> {
|
||||
if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) {
|
||||
throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES);
|
||||
}
|
||||
|
||||
const restaurant = await this.getRestaurantOrFail(restaurantId);
|
||||
|
||||
const serviceArea = restaurant.serviceArea;
|
||||
// If no service area is defined, assume service is available everywhere
|
||||
if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return;
|
||||
|
||||
// GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring
|
||||
const rings = serviceArea.coordinates;
|
||||
if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return;
|
||||
|
||||
const ring = rings[0];
|
||||
const point: [number, number] = [Number(longitude), Number(latitude)];
|
||||
|
||||
// Ensure polygon has the correct tuple typing ([lng, lat] pairs)
|
||||
const polygon: [number, number][] = ring.map(
|
||||
coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number],
|
||||
);
|
||||
|
||||
if (!GeographicUtils.isPointInPolygon(point, polygon)) {
|
||||
throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delivery method for restaurant or throw if not found
|
||||
*/
|
||||
async getDeliveryMethodForRestaurantOrFail(
|
||||
restaurantId: string,
|
||||
deliveryMethodId: string,
|
||||
): Promise<Delivery> {
|
||||
const deliveryMethod = await this.em.findOne(Delivery, {
|
||||
id: deliveryMethodId,
|
||||
restaurant: { id: restaurantId },
|
||||
});
|
||||
|
||||
if (!deliveryMethod) {
|
||||
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT);
|
||||
}
|
||||
|
||||
return deliveryMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled delivery method or throw if not found or disabled
|
||||
*/
|
||||
async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise<Delivery> {
|
||||
const deliveryMethod = await this.em.findOne(
|
||||
Delivery,
|
||||
{ id: deliveryMethodId, restaurant: { id: restaurantId } },
|
||||
{ populate: ['restaurant'] },
|
||||
);
|
||||
if (!deliveryMethod) {
|
||||
throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND);
|
||||
}
|
||||
if (!deliveryMethod.enabled) {
|
||||
throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED);
|
||||
}
|
||||
return deliveryMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert delivery method matches expected type
|
||||
*/
|
||||
assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require delivery method ID or throw
|
||||
*/
|
||||
requireDeliveryMethodId(cart: Cart, message: string): string {
|
||||
if (!cart.deliveryMethodId) {
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
return cart.deliveryMethodId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled payment method or throw if not found or disabled
|
||||
*/
|
||||
async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise<PaymentMethod> {
|
||||
const paymentMethod = await this.em.findOne(
|
||||
PaymentMethod,
|
||||
{ id: paymentMethodId, restaurant: { id: restaurantId } },
|
||||
{ populate: ['restaurant'] },
|
||||
);
|
||||
if (!paymentMethod) {
|
||||
throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND);
|
||||
}
|
||||
if (!paymentMethod.enabled) {
|
||||
throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED);
|
||||
}
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert wallet has enough balance
|
||||
*/
|
||||
async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise<void> {
|
||||
const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId);
|
||||
|
||||
if (balance < amount) {
|
||||
throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert coupon usage limit
|
||||
*/
|
||||
async assertCouponUsageLimit(userId: string, couponId: string, maxUsesPerUser?: number): Promise<void> {
|
||||
if (!maxUsesPerUser) return;
|
||||
|
||||
const ordersThatUsedTheCouponCount = await this.em.count(Order, {
|
||||
user: { id: userId },
|
||||
couponDetail: { couponId: couponId },
|
||||
status: { $ne: OrderStatus.CANCELED },
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
if (ordersThatUsedTheCouponCount >= maxUsesPerUser) {
|
||||
throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert coupon matches cart if restricted
|
||||
*/
|
||||
async assertCouponMatchesCartIfRestricted(
|
||||
cart: Cart,
|
||||
foodCategories?: string[] | null,
|
||||
foods?: string[] | null,
|
||||
): Promise<void> {
|
||||
const categoryRestriction = foodCategories?.filter(Boolean) ?? [];
|
||||
const foodRestriction = foods?.filter(Boolean) ?? [];
|
||||
const hasCategoryRestriction = categoryRestriction.length > 0;
|
||||
const hasFoodRestriction = foodRestriction.length > 0;
|
||||
if (!hasCategoryRestriction && !hasFoodRestriction) return;
|
||||
|
||||
const foodMap = await this.getFoodsInCartWithCategories(cart);
|
||||
|
||||
for (const item of cart.items) {
|
||||
const food = foodMap.get(item.foodId);
|
||||
if (!food) continue;
|
||||
|
||||
const matchesCategory = hasCategoryRestriction && food.category && categoryRestriction.includes(food.category.id);
|
||||
const matchesFood = hasFoodRestriction && foodRestriction.includes(food.id);
|
||||
if (matchesCategory || matchesFood) return;
|
||||
}
|
||||
|
||||
throw new BadRequestException(CartMessage.COUPON_CANNOT_BE_APPLIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foods in cart with categories
|
||||
*/
|
||||
async getFoodsInCartWithCategories(cart: Cart): Promise<Map<string, Food>> {
|
||||
const foodIds = cart.items.map(item => item.foodId);
|
||||
if (foodIds.length === 0) return new Map();
|
||||
|
||||
const foods = await this.em.find(Food, { id: { $in: foodIds } }, { populate: ['category'] });
|
||||
return new Map(foods.map(f => [f.id, f]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert all foods in cart have pickupServe true when delivery method is courier
|
||||
*/
|
||||
async assertAllFoodsHavePickupServeForCourier(cart: Cart, deliveryMethod: DeliveryMethodEnum): Promise<void> {
|
||||
if (deliveryMethod !== DeliveryMethodEnum.DeliveryCourier) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cart.items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foodIds = cart.items.map(item => item.foodId);
|
||||
const foods = await this.em.find(Food, { id: { $in: foodIds } });
|
||||
|
||||
const foodsWithoutPickupServe = foods.filter(food => !food.pickupServe);
|
||||
|
||||
if (foodsWithoutPickupServe.length > 0) {
|
||||
const foodTitles = foodsWithoutPickupServe.map(f => f.title || f.id).join(', ');
|
||||
throw new BadRequestException(
|
||||
CartMessage.FOODS_MUST_HAVE_PICKUP_SERVE_FOR_COURIER.replace('[foodTitles]', foodTitles),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Iran time context (weekday and meal type)
|
||||
*/
|
||||
private getCurrentIranTimeContext(): { iranWeekDay: number; mealType: MealType | null } {
|
||||
const nowInIran = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tehran' }));
|
||||
const weekDay = nowInIran.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const currentHour = nowInIran.getHours();
|
||||
|
||||
// Convert to Iran weekday: Saturday=0, Sunday=1, ..., Friday=6
|
||||
// JavaScript: Sunday=0, Monday=1, ..., Saturday=6
|
||||
// Iran week: Saturday=0, Sunday=1, ..., Friday=6
|
||||
const iranWeekDay = (weekDay + 1) % 7;
|
||||
|
||||
const mealType = this.getMealTypeByHour(currentHour);
|
||||
|
||||
return { iranWeekDay, mealType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine meal type based on current hour in Iran timezone.
|
||||
* @param hour - Current hour (0-23)
|
||||
* @returns Meal type or null if outside meal hours
|
||||
*/
|
||||
private getMealTypeByHour(hour: number): MealType | null {
|
||||
const MEAL_TIME_RANGES = {
|
||||
BREAKFAST: { start: 6, end: 11 },
|
||||
LUNCH: { start: 11, end: 15 },
|
||||
SNACK: { start: 15, end: 19 },
|
||||
DINNER: { start: 19, end: 24 },
|
||||
} as const;
|
||||
|
||||
if (hour >= MEAL_TIME_RANGES.BREAKFAST.start && hour < MEAL_TIME_RANGES.BREAKFAST.end) {
|
||||
return MealType.BREAKFAST;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.LUNCH.start && hour < MEAL_TIME_RANGES.LUNCH.end) {
|
||||
return MealType.LUNCH;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.SNACK.start && hour < MEAL_TIME_RANGES.SNACK.end) {
|
||||
return MealType.SNACK;
|
||||
}
|
||||
if (hour >= MEAL_TIME_RANGES.DINNER.start && hour < MEAL_TIME_RANGES.DINNER.end) {
|
||||
return MealType.DINNER;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert meal type compatibility - if food has mealTypes, current meal time must be in that array
|
||||
*/
|
||||
assertMealTypeCompatibility(food: Food): void {
|
||||
// If food has no meal type restrictions, allow it
|
||||
if (!food.mealTypes || food.mealTypes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mealType } = this.getCurrentIranTimeContext();
|
||||
|
||||
// If current time is outside meal hours, throw error
|
||||
if (mealType === null) {
|
||||
const foodTitle = food.title || food.id;
|
||||
throw new BadRequestException(
|
||||
CartMessage.FOOD_ONLY_AVAILABLE_DURING_MEAL_TIMES.replace('[foodTitle]', foodTitle),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if current meal type is in food's allowed meal types
|
||||
if (!food.mealTypes.includes(mealType)) {
|
||||
const foodTitle = food.title || food.id;
|
||||
const mealTypeMap: Record<MealType, string> = {
|
||||
[MealType.BREAKFAST]: 'صبحانه',
|
||||
[MealType.LUNCH]: 'ناهار',
|
||||
[MealType.SNACK]: 'عصرانه',
|
||||
[MealType.DINNER]: 'شام',
|
||||
};
|
||||
const allowedMealTypes = food.mealTypes.map(mt => mealTypeMap[mt]).join(', ');
|
||||
const currentMealType = mealType ? mealTypeMap[mealType] : mealType;
|
||||
throw new BadRequestException(
|
||||
CartMessage.FOOD_ONLY_AVAILABLE_FOR_MEAL_TYPES.replace('[foodTitle]', foodTitle)
|
||||
.replace('[allowedMealTypes]', allowedMealTypes)
|
||||
.replace('[mealType]', currentMealType),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert weekday compatibility - current weekday must be in food's weekDays array
|
||||
*/
|
||||
assertWeekdayCompatibility(food: Food): void {
|
||||
// If food has no weekday restrictions (empty array or all days), allow it
|
||||
if (!food.weekDays || food.weekDays.length === 0 || food.weekDays.length === 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { iranWeekDay } = this.getCurrentIranTimeContext();
|
||||
|
||||
// Check if current weekday is in food's allowed weekdays
|
||||
if (!food.weekDays.includes(iranWeekDay)) {
|
||||
const dayNames = ['شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'];
|
||||
const currentDayName = dayNames[iranWeekDay];
|
||||
const allowedDays = food.weekDays.map(day => dayNames[day]).join(', ');
|
||||
const foodTitle = food.title || food.id;
|
||||
throw new BadRequestException(
|
||||
CartMessage.FOOD_ONLY_AVAILABLE_ON_DAYS.replace('[foodTitle]', foodTitle)
|
||||
.replace('[allowedDays]', allowedDays)
|
||||
.replace('[currentDay]', currentDayName),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { DeliveryMethodEnum } from '../../delivery/interface/delivery';
|
||||
import { PaymentMethodEnum } from '../../payments/interface/payment';
|
||||
import { OrderCouponDetail } from '../../orders/interface/order.interface';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
import { BulkAddItemsToCartDto } from '../dto/bulk-add-items.dto';
|
||||
import { ApplyCouponDto } from '../dto/apply-coupon.dto';
|
||||
import { SetAllCartParmsDto } from '../dto/set-all-cart-params.dto';
|
||||
import { CouponService } from '../../coupons/providers/coupon.service';
|
||||
import { CartRepository } from '../repositories/cart.repository';
|
||||
import { CartValidationService } from './cart-validation.service';
|
||||
import { CartCalculationService } from './cart-calculation.service';
|
||||
import { CartItemService } from './cart-item.service';
|
||||
import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto';
|
||||
import { CartMessage } from 'src/common/enums/message.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CartService {
|
||||
constructor(
|
||||
private readonly cartRepository: CartRepository,
|
||||
private readonly validationService: CartValidationService,
|
||||
private readonly calculationService: CartCalculationService,
|
||||
private readonly itemService: CartItemService,
|
||||
private readonly couponService: CouponService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Set all cart parameters at once
|
||||
*/
|
||||
async setAllCartParams(userId: string, restaurantId: string, params: SetAllCartParmsDto): Promise<Cart> {
|
||||
const { deliveryMethodId, paymentMethodId, addressId, carAddress, description, tableNumber } = params;
|
||||
|
||||
// get existing cart (or throw)
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
// Validate and get delivery method
|
||||
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
|
||||
restaurantId,
|
||||
deliveryMethodId,
|
||||
);
|
||||
cart.deliveryMethodId = deliveryMethodId;
|
||||
|
||||
// Handle each delivery method with its specific requirements
|
||||
switch (deliveryMethod.method) {
|
||||
case DeliveryMethodEnum.DineIn:
|
||||
// DineIn requires table number and clears incompatible fields
|
||||
if (!tableNumber) {
|
||||
throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED);
|
||||
}
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
|
||||
cart.tableNumber = tableNumber;
|
||||
break;
|
||||
|
||||
case DeliveryMethodEnum.CustomerPickup:
|
||||
// CustomerPickup just clears incompatible fields
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup);
|
||||
break;
|
||||
|
||||
case DeliveryMethodEnum.DeliveryCar:
|
||||
// DeliveryCar requires carAddress and clears incompatible fields
|
||||
if (!carAddress) {
|
||||
throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED);
|
||||
}
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
|
||||
const user = await this.validationService.getUserOrFail(userId);
|
||||
cart.carAddress = {
|
||||
carModel: carAddress.carModel,
|
||||
carColor: carAddress.carColor,
|
||||
plateNumber: carAddress.plateNumber,
|
||||
phone: user.phone,
|
||||
};
|
||||
break;
|
||||
|
||||
case DeliveryMethodEnum.DeliveryCourier:
|
||||
// DeliveryCourier requires addressId and clears incompatible fields
|
||||
if (!addressId) {
|
||||
throw new BadRequestException(CartMessage.ADDRESS_REQUIRED);
|
||||
}
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
||||
const address = await this.validationService.getUserAddressOrFail(addressId);
|
||||
this.validationService.validateAddressOwnership(address, userId);
|
||||
|
||||
// ensure address is within restaurant service area
|
||||
await this.validationService.assertAddressInsideServiceArea(
|
||||
restaurantId,
|
||||
address.latitude,
|
||||
address.longitude,
|
||||
);
|
||||
|
||||
cart.userAddress = {
|
||||
address: address.address,
|
||||
latitude: address.latitude,
|
||||
longitude: address.longitude,
|
||||
city: address.city,
|
||||
province: address.province || '',
|
||||
postalCode: address.postalCode || undefined,
|
||||
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
|
||||
phone: address.user.phone,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate and set payment method
|
||||
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
|
||||
restaurantId,
|
||||
paymentMethodId,
|
||||
);
|
||||
|
||||
// Recalculate totals first so wallet check uses up-to-date total (delivery method may have changed above).
|
||||
await this.calculationService.recalculateCartTotals(cart);
|
||||
|
||||
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
|
||||
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
|
||||
}
|
||||
|
||||
cart.paymentMethodId = paymentMethodId;
|
||||
|
||||
// Set description (if provided)
|
||||
if (description !== undefined) {
|
||||
cart.description = description;
|
||||
}
|
||||
|
||||
// Final validation: if effective delivery method is courier, ensure all foods have pickupServe
|
||||
await this.validationService.assertAllFoodsHavePickupServeForCourier(cart, deliveryMethod.method);
|
||||
|
||||
// Final recalculation + save and return cart
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create cart for user and restaurant
|
||||
*/
|
||||
async getOrCreateCart(userId: string, restaurantId: string): Promise<Cart> {
|
||||
const existingCart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
|
||||
if (existingCart) {
|
||||
return existingCart;
|
||||
}
|
||||
|
||||
// Find restaurant
|
||||
const restaurant = await this.validationService.getRestaurantOrFail(restaurantId);
|
||||
|
||||
// Create new cart
|
||||
const now = this.nowIso();
|
||||
const cart: Cart = {
|
||||
userId,
|
||||
restaurantId,
|
||||
restaurantName: restaurant.name,
|
||||
items: [],
|
||||
deliveryFee: 0,
|
||||
subTotal: 0,
|
||||
itemsDiscount: 0,
|
||||
couponDiscount: 0,
|
||||
totalDiscount: 0,
|
||||
tax: 0,
|
||||
total: 0,
|
||||
totalItems: 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await this.cartRepository.save(cart);
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cart by user and restaurant
|
||||
*/
|
||||
async findOneOrFail(userId: string, restaurantId: string): Promise<Cart> {
|
||||
const cart = await this.cartRepository.findByUserAndRestaurant(userId, restaurantId);
|
||||
if (!cart) {
|
||||
throw new NotFoundException(CartMessage.NOT_FOUND);
|
||||
}
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment item quantity in cart
|
||||
*/
|
||||
async incrementItem(userId: string, restaurantId: string, foodId: string, quantity: number): Promise<Cart> {
|
||||
const cart = await this.getOrCreateCart(userId, restaurantId);
|
||||
await this.itemService.addOrIncrementItem(cart, foodId, restaurantId, quantity);
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement item quantity in cart by 1 (removes item if quantity reaches 0)
|
||||
*/
|
||||
async decrementItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
await this.itemService.decrementOrRemoveItem(cart, foodId);
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk add items to cart (increments quantity if items exist)
|
||||
*/
|
||||
async bulkAddItems(userId: string, restaurantId: string, bulkAddItemsDto: BulkAddItemsToCartDto): Promise<Cart> {
|
||||
const cart = await this.getOrCreateCart(userId, restaurantId);
|
||||
|
||||
for (const addItemDto of bulkAddItemsDto.items) {
|
||||
await this.itemService.addOrIncrementItem(cart, addItemDto.foodId, restaurantId, addItemDto.quantity);
|
||||
}
|
||||
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item from cart
|
||||
*/
|
||||
async removeItem(userId: string, restaurantId: string, foodId: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
this.itemService.removeItemOrFail(cart, foodId);
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply coupon to cart
|
||||
*/
|
||||
async applyCoupon(userId: string, restaurantId: string, applyCouponDto: ApplyCouponDto): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const orderAmount = cart.subTotal - cart.itemsDiscount;
|
||||
const user = await this.validationService.getUserOrFail(userId);
|
||||
// check if coupon is valid and belong to the restaurant
|
||||
const { valid, coupon, message } = await this.couponService.validateCoupon(
|
||||
applyCouponDto.code,
|
||||
restaurantId,
|
||||
Number(orderAmount),
|
||||
user.phone,
|
||||
);
|
||||
|
||||
if (!valid) {
|
||||
throw new BadRequestException(message || CartMessage.COUPON_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!coupon) {
|
||||
throw new BadRequestException(CartMessage.COUPON_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Check maxUsesPerUser limit by counting how many orders this user has with this coupon
|
||||
await this.validationService.assertCouponUsageLimit(userId, coupon.id, coupon.maxUsesPerUser);
|
||||
await this.validationService.assertCouponMatchesCartIfRestricted(
|
||||
cart,
|
||||
coupon.foodCategories,
|
||||
coupon.foods,
|
||||
);
|
||||
|
||||
const couponDetail: OrderCouponDetail = {
|
||||
couponId: coupon.id,
|
||||
couponName: coupon.name,
|
||||
couponCode: coupon.code,
|
||||
type: coupon.type,
|
||||
value: coupon.value,
|
||||
maxDiscount: coupon.maxDiscount,
|
||||
};
|
||||
|
||||
cart.coupon = couponDetail;
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove coupon from cart
|
||||
*/
|
||||
async removeCoupon(userId: string, restaurantId: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
cart.coupon = undefined;
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set address for cart
|
||||
*/
|
||||
async setAddress(userId: string, restaurantId: string, addressId: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
|
||||
cart,
|
||||
'Delivery method must be set before setting address',
|
||||
);
|
||||
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
||||
restaurantId,
|
||||
deliveryMethodId,
|
||||
);
|
||||
this.validationService.assertDeliveryMethod(
|
||||
deliveryMethod.method,
|
||||
DeliveryMethodEnum.DeliveryCourier,
|
||||
'Delivery method must be DeliveryCourier',
|
||||
);
|
||||
|
||||
// Find and validate address belongs to user
|
||||
const address = await this.validationService.getUserAddressOrFail(addressId);
|
||||
|
||||
// Verify address belongs to the user
|
||||
this.validationService.validateAddressOwnership(address, userId);
|
||||
|
||||
// ensure address is within restaurant service area
|
||||
await this.validationService.assertAddressInsideServiceArea(
|
||||
restaurantId,
|
||||
address.latitude,
|
||||
address.longitude,
|
||||
);
|
||||
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCourier);
|
||||
cart.userAddress = {
|
||||
address: address.address,
|
||||
latitude: address.latitude,
|
||||
longitude: address.longitude,
|
||||
city: address.city,
|
||||
province: address.province || '',
|
||||
postalCode: address.postalCode || undefined,
|
||||
fullName: `${address.user.firstName || ''} ${address.user.lastName || ''}`.trim(),
|
||||
phone: address.user.phone,
|
||||
};
|
||||
return this.saveTouchedCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set payment method for cart
|
||||
*/
|
||||
async setPaymentMethod(
|
||||
userId: string,
|
||||
restaurantId: string,
|
||||
paymentMethodId: string,
|
||||
): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const paymentMethod = await this.validationService.getEnabledPaymentMethodOrFail(
|
||||
restaurantId,
|
||||
paymentMethodId,
|
||||
);
|
||||
if (paymentMethod.method === PaymentMethodEnum.Wallet) {
|
||||
await this.validationService.assertWalletHasEnoughBalance(userId, restaurantId, cart.total);
|
||||
}
|
||||
cart.paymentMethodId = paymentMethodId;
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set delivery method for cart
|
||||
*/
|
||||
async setDeliveryMethod(
|
||||
userId: string,
|
||||
restaurantId: string,
|
||||
deliveryMethodId: string,
|
||||
): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const deliveryMethod = await this.validationService.getEnabledDeliveryMethodOrFail(
|
||||
restaurantId,
|
||||
deliveryMethodId,
|
||||
);
|
||||
|
||||
cart.deliveryMethodId = deliveryMethodId;
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, deliveryMethod.method);
|
||||
|
||||
return this.recalculateAndSaveCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description for cart
|
||||
*/
|
||||
async setDescription(userId: string, restaurantId: string, description: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
cart.description = description;
|
||||
return this.saveTouchedCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set table number for cart
|
||||
*/
|
||||
async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
|
||||
cart,
|
||||
'Delivery method must be set before setting table number',
|
||||
);
|
||||
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
||||
restaurantId,
|
||||
deliveryMethodId,
|
||||
);
|
||||
this.validationService.assertDeliveryMethod(
|
||||
deliveryMethod.method,
|
||||
DeliveryMethodEnum.DineIn,
|
||||
'Delivery method must be DineIn',
|
||||
);
|
||||
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn);
|
||||
cart.tableNumber = tableNumber;
|
||||
return this.saveTouchedCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set car delivery for cart
|
||||
*/
|
||||
async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise<Cart> {
|
||||
const cart = await this.findOneOrFail(userId, restaurantId);
|
||||
|
||||
const deliveryMethodId = this.validationService.requireDeliveryMethodId(
|
||||
cart,
|
||||
'Delivery method must be set before setting car delivery',
|
||||
);
|
||||
const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail(
|
||||
restaurantId,
|
||||
deliveryMethodId,
|
||||
);
|
||||
this.validationService.assertDeliveryMethod(
|
||||
deliveryMethod.method,
|
||||
DeliveryMethodEnum.DeliveryCar,
|
||||
'Delivery method must be DeliveryCar',
|
||||
);
|
||||
const user = await this.validationService.getUserOrFail(userId);
|
||||
|
||||
this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar);
|
||||
cart.carAddress = {
|
||||
carModel: setCarDeliveryDto.carModel,
|
||||
carColor: setCarDeliveryDto.carColor,
|
||||
plateNumber: setCarDeliveryDto.plateNumber,
|
||||
phone: user.phone,
|
||||
};
|
||||
return this.saveTouchedCart(cart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cart from cache
|
||||
*/
|
||||
async clearCart(userId: string, restaurantId: string): Promise<void> {
|
||||
return this.cartRepository.delete(userId, restaurantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears cart fields that are not applicable for a given delivery method.
|
||||
*/
|
||||
private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void {
|
||||
if (method === DeliveryMethodEnum.DineIn) {
|
||||
cart.userAddress = null;
|
||||
cart.carAddress = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === DeliveryMethodEnum.CustomerPickup) {
|
||||
cart.userAddress = null;
|
||||
cart.carAddress = null;
|
||||
cart.tableNumber = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === DeliveryMethodEnum.DeliveryCourier) {
|
||||
cart.carAddress = null;
|
||||
cart.tableNumber = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === DeliveryMethodEnum.DeliveryCar) {
|
||||
cart.userAddress = null;
|
||||
cart.tableNumber = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cart with updated timestamp
|
||||
*/
|
||||
private async saveTouchedCart(cart: Cart): Promise<Cart> {
|
||||
cart.updatedAt = this.nowIso();
|
||||
await this.cartRepository.save(cart);
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate cart totals and save
|
||||
*/
|
||||
private async recalculateAndSaveCart(cart: Cart): Promise<Cart> {
|
||||
await this.calculationService.recalculateCartTotals(cart);
|
||||
await this.cartRepository.save(cart);
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current ISO timestamp
|
||||
*/
|
||||
private nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CacheService } from '../../utils/cache.service';
|
||||
import { Cart } from '../interfaces/cart.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CartRepository {
|
||||
private readonly CART_TTL = 3600; // 1 hour in seconds
|
||||
private readonly CART_KEY_PREFIX = 'cart';
|
||||
|
||||
constructor(private readonly cacheService: CacheService) {}
|
||||
|
||||
/**
|
||||
* Get cart by user and restaurant
|
||||
*/
|
||||
async findByUserAndRestaurant(userId: string, restaurantId: string): Promise<Cart | null> {
|
||||
const cacheKey = this.getCacheKey(userId, restaurantId);
|
||||
const cachedCart = await this.cacheService.get<string>(cacheKey);
|
||||
|
||||
if (cachedCart) {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(cachedCart);
|
||||
if (this.isCart(parsed) && parsed.userId === userId && parsed.restaurantId === restaurantId) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, return null
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cart to cache
|
||||
*/
|
||||
async save(cart: Cart): Promise<void> {
|
||||
const cacheKey = this.getCacheKey(cart.userId, cart.restaurantId);
|
||||
await this.cacheService.set(cacheKey, JSON.stringify(cart), this.CART_TTL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cart from cache
|
||||
*/
|
||||
async delete(userId: string, restaurantId: string): Promise<void> {
|
||||
const cacheKey = this.getCacheKey(userId, restaurantId);
|
||||
await this.cacheService.del(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for cart
|
||||
*/
|
||||
private getCacheKey(userId: string, restaurantId: string): string {
|
||||
return `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a Cart
|
||||
*/
|
||||
private isCart(obj: unknown): obj is Cart {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
const cart = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof cart.userId === 'string' &&
|
||||
typeof cart.restaurantId === 'string' &&
|
||||
Array.isArray(cart.items) &&
|
||||
typeof cart.subTotal === 'number' &&
|
||||
typeof cart.itemsDiscount === 'number' &&
|
||||
typeof cart.couponDiscount === 'number' &&
|
||||
typeof cart.totalDiscount === 'number' &&
|
||||
typeof cart.tax === 'number' &&
|
||||
typeof cart.deliveryFee === 'number' &&
|
||||
typeof cart.total === 'number' &&
|
||||
typeof cart.totalItems === 'number' &&
|
||||
typeof cart.createdAt === 'string' &&
|
||||
typeof cart.updatedAt === 'string'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Utility class for geographic calculations
|
||||
*/
|
||||
export class GeographicUtils {
|
||||
/**
|
||||
* Check if a point is inside a polygon using ray-casting algorithm
|
||||
* @param point - [longitude, latitude]
|
||||
* @param polygon - Array of [longitude, latitude] pairs
|
||||
*/
|
||||
static isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean {
|
||||
const x = point[0];
|
||||
const y = point[1];
|
||||
let inside = false;
|
||||
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i][0];
|
||||
const yi = polygon[i][1];
|
||||
const xj = polygon[j][0];
|
||||
const yj = polygon[j][1];
|
||||
|
||||
const intersect =
|
||||
yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi + Number.EPSILON) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians
|
||||
*/
|
||||
private static toRad(value: number): number {
|
||||
return (value * Math.PI) / 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points in kilometers using Haversine formula
|
||||
*/
|
||||
static getDistanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const R = 6371; // Earth radius in KM
|
||||
|
||||
const dLat = this.toRad(lat2 - lat1);
|
||||
const dLng = this.toRad(lng2 - lng1);
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) ** 2;
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points in kilometers (rounded)
|
||||
*/
|
||||
static getDistanceKmRounded(
|
||||
lat1: number,
|
||||
lng1: number,
|
||||
lat2: number,
|
||||
lng2: number,
|
||||
precision = 2,
|
||||
): number {
|
||||
const distance = this.getDistanceKm(lat1, lng1, lat2, lng2);
|
||||
return Number(distance.toFixed(precision));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ContactService } from './providers/contact.service';
|
||||
import { ContactController } from './controllers/contact.controller';
|
||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||
import { Contact } from './entities/contact.entity';
|
||||
import { ContactRepository } from './repositories/contact.repository';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Contact]), JwtModule],
|
||||
controllers: [ContactController],
|
||||
providers: [ContactService, ContactRepository],
|
||||
exports: [ContactRepository],
|
||||
})
|
||||
export class ContactModule {}
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { ContactService } from '../providers/contact.service';
|
||||
import { CreateContactDto } from '../dto/create-contact.dto';
|
||||
import { FindContactsDto } from '../dto/find-contacts.dto';
|
||||
import { UpdateContactStatusDto } from '../dto/update-contact-status.dto';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiHeader } from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||
import { RestSlug } from 'src/common/decorators/rest-slug.decorator';
|
||||
|
||||
@ApiTags('contact')
|
||||
@Controller()
|
||||
export class ContactController {
|
||||
constructor(private readonly contactService: ContactService) { }
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('public/contact')
|
||||
@ApiOperation({ summary: 'Create a new contact (public endpoint)' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
async create(@Body() createContactDto: CreateContactDto, @RestSlug() slug: string, @RestId() restId?: string) {
|
||||
return this.contactService.create(createContactDto, restId, slug);
|
||||
}
|
||||
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CONTACTS)
|
||||
@Get('admin/contact')
|
||||
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
|
||||
async findAllPaginated(@RestId() restId: string, @Query() dto: FindContactsDto) {
|
||||
return this.contactService.findAllPaginatedAdmin(dto, restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CONTACTS)
|
||||
@Get('admin/contact/:id')
|
||||
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
|
||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
||||
async findOne(@RestId() restId: string, @Param('id') id: string) {
|
||||
return this.contactService.findOne(id, restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CONTACTS)
|
||||
@Patch('admin/contact/:id/status')
|
||||
@ApiOperation({ summary: 'Update contact status (admin only)' })
|
||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
||||
async updateStatus(@RestId() restId: string, @Param('id') id: string, @Body() dto: UpdateContactStatusDto) {
|
||||
return this.contactService.updateStatus(id, dto, restId);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Permissions(Permission.MANAGE_CONTACTS)
|
||||
@Delete('admin/contact/:id')
|
||||
@ApiOperation({ summary: 'Hard delete a contact (admin only)' })
|
||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
||||
async remove(@RestId() restId: string, @Param('id') id: string) {
|
||||
await this.contactService.remove(id, restId);
|
||||
}
|
||||
|
||||
/*** Super Admin ***/
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('super-admin/contact')
|
||||
@ApiOperation({ summary: 'Get paginated list of contacts (admin only)' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'search', required: false, type: String })
|
||||
@ApiQuery({ name: 'orderBy', required: false, type: String })
|
||||
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
|
||||
@ApiQuery({ name: 'status', required: false, enum: ['new', 'seen'] })
|
||||
async findAllPaginatedForSuper(@Query() dto: FindContactsDto) {
|
||||
return this.contactService.findAllPaginatedSuper(dto);
|
||||
}
|
||||
|
||||
@UseGuards(SuperAdminAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@Get('super-admin/contact/:id')
|
||||
@ApiOperation({ summary: 'Get contact details by ID (admin only)' })
|
||||
@ApiParam({ name: 'id', description: 'Contact ID' })
|
||||
async findOneForSuper(@Param('id') id: string) {
|
||||
return this.contactService.findOne(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IsNotEmpty, IsString, IsOptional, IsMobilePhone, IsEnum } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ContactScope } from '../interface/interface';
|
||||
|
||||
export class CreateContactDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'Question about menu', description: 'Contact subject' })
|
||||
subject?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'I have a question about your menu items', description: 'Contact content/message' })
|
||||
content!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(['application', 'restaurant'])
|
||||
@ApiProperty({ description: 'Contact Scope' })
|
||||
scope!: ContactScope;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsMobilePhone('fa-IR')
|
||||
@ApiPropertyOptional({ example: '09362532122', description: 'Phone number' })
|
||||
phone?: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { IsOptional, IsNumber, IsString, IsIn, IsEnum } from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ContactStatusEnum } from '../interface/interface';
|
||||
|
||||
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||
type SortOrder = (typeof sortOrderOptions)[number];
|
||||
|
||||
export class FindContactsDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 1, description: 'Page number', minimum: 1, default: 1 })
|
||||
page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ example: 10, description: 'Items per page', minimum: 1, default: 10 })
|
||||
limit?: number = 10;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'menu', description: 'Search in subject, content, or phone' })
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ example: 'createdAt', description: 'Field to sort by', default: 'createdAt' })
|
||||
orderBy?: string = 'createdAt';
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(sortOrderOptions)
|
||||
@ApiPropertyOptional({ example: 'desc', enum: sortOrderOptions, default: 'desc' })
|
||||
order?: SortOrder = 'desc';
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ContactStatusEnum)
|
||||
@ApiPropertyOptional({ enum: ContactStatusEnum, description: 'Filter by status' })
|
||||
status?: ContactStatusEnum;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ContactStatusEnum } from '../interface/interface';
|
||||
|
||||
export class UpdateContactStatusDto {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(ContactStatusEnum)
|
||||
@ApiProperty({ enum: ContactStatusEnum, description: 'New status for the contact' })
|
||||
status!: ContactStatusEnum;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateContactDto } from './create-contact.dto';
|
||||
|
||||
export class UpdateContactDto extends PartialType(CreateContactDto) {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user