This commit is contained in:
mahyargdz
2025-09-14 15:15:03 +03:30
commit be82059172
534 changed files with 51310 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = LF
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4
# Matches the exact files either *.json or *.yml
[*.{json,yml},*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
+53
View File
@@ -0,0 +1,53 @@
NODE_ENV=development
PORT=4000
#DB
# MONGO_URI=mongodb://root:pXuivdePAjReP4iJ7e0m5Lg4LxA@localhost:27017/shinan?authSource=admin&replicaSet=rs0&directConnection=true
REDIS_URI=redis://:@localhost:6379/0
REDIS_HOST=
REDIS_PORT=
REDIS_PASSWORD=
#SMS
SMS_API_KEY=
SMS_NUMBER=
SMS_PATTERN_OTP=
SMS_PATTERN_USER_ORDER_PEND=
SMS_PATTERN_USER_ORDER_CREATE=
SMS_PATTERN_USER_ORDER_SHIPPED=
SMS_PATTERN_USER_ORDER_RETURNED=
SMS_PATTERN_SELLER_ORDER_CREATE=
SMS_PATTERN_SELLER_ORDER_CANCELLED=
#EMAIL
SMTP_EMAIL=info@shinan.ir
SMTP_HOST=smtp.c1.liara.email
SMTP_PORT=
SMTPS_PORT=
SMTP_USER=
SMTP_PASSWORD=
#JWT
JWT_SECRET=
JWT_EXPIRE_ACCESS=2 #minutes
JWT_EXPIRE_REFRESH=2 #days
#S3_BUCKET
BUCKET_NAME=
BUCKET_URL=
BUCKET_ACCESS_KEY=
BUCKET_SECRET_KEY=
#DEFAULT_ADMIN
DEFAULT_ADMIN_EMAIL=
DEFAULT_ADMIN_PASSWORD=
#PAYMENT_GATEWAY
IPG_TYPE=sandbox #sandbox or payments
SITE_URL=https://api.shinan.ir/payment
STORE_URL=https://shinan.com
SELLER_URL=https://seller.shinan.com
ZARINPAL_MERCHANT_ID=
ASANPARDAKHT_MERCHANT_ID=
ASANPARDAKHT_MERCHANT_CONFIG_ID=
ASANPARDAKHT_USER=
ASANPARDAKHT_PASS=
#MAP
MAP_API_KEY=
NESHAN_API_KEY=
LOGGER_LEVEL=info
+5
View File
@@ -0,0 +1,5 @@
node_modules
dist
.eslintrc.js
.prettierrc.js
commitlint.config.ts
+98
View File
@@ -0,0 +1,98 @@
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "prettier", "import"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
],
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
env: {
es6: true,
node: true,
},
rules: {
// turn on errors for missing imports
"sort-imports": [
"error",
{
ignoreCase: false,
ignoreDeclarationSort: true,
ignoreMemberSort: false,
memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
allowSeparatedGroups: true,
},
],
"import/no-unresolved": "error",
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", ["sibling", "parent"], "index", "unknown"],
"newlines-between": "always",
alphabetize: {
/* sort in ascending order. Options: ["ignore", "asc", "desc"] */
order: "asc",
/* ignore case. Options: [true, false] */
caseInsensitive: true,
},
},
],
// JavaScript Rules
"no-var": "error",
"prefer-const": "error",
"no-multi-spaces": "error",
"space-in-parens": "error",
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0 }],
semi: ["error", "always"],
// Best Practices
"no-async-promise-executor": "warn",
"no-control-regex": "warn",
"no-empty": ["warn", { allowEmptyCatch: true }],
"no-extra-semi": "warn",
"no-prototype-builtins": "warn",
"no-regex-spaces": "warn",
"prefer-rest-params": "warn",
"prefer-spread": "warn",
// TypeScript-Specific Rules
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-misused-new": "warn",
"@typescript-eslint/no-namespace": "warn",
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
"@typescript-eslint/no-unsafe-declaration-merging": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/triple-slash-reference": "warn",
// "@typescript-eslint/explicit-function-return-type": [
// "warn",
// {
// allowExpressions: true,
// allowTypedFunctionExpressions: true,
// },
// ],
// "@typescript-eslint/no-inferrable-types": "warn",
// "@typescript-eslint/no-non-null-assertion": "warn",
},
};
+47
View File
@@ -0,0 +1,47 @@
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
DANAK_SERVER: "https://captain.dev.danakcorp.com"
APP_TOKEN: 28cd7dee97fba06d2ca2e1de3f90ad6bd586b8b219ab7d13634d3096639637c4
CAPROVER_APP_NAME: shop-api
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 $CAPROVER_APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL"
+147
View File
@@ -0,0 +1,147 @@
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.env
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
.vscode
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
none/
qodana.yaml
bunfig.toml
reflect-metadata-import.ts
.idea
.celp-ai
page.html
.DS_Store
html
+1
View File
@@ -0,0 +1 @@
npx --no-install commitlint --edit $1
+2
View File
@@ -0,0 +1,2 @@
npx lint-staged
+9
View File
@@ -0,0 +1,9 @@
{
"tabWidth": 2,
"semi": true,
"arrowParens": "always",
"singleQuote": false,
"trailingComma": "all",
"printWidth": 140,
"endOfLine": "auto"
}
+41
View File
@@ -0,0 +1,41 @@
# Stage 1: Build Stage
FROM node:22-alpine AS base
RUN npm install -g corepack@latest
RUN corepack enable && corepack prepare pnpm@latest --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 4000
CMD ["npm", "start"]
+19
View File
@@ -0,0 +1,19 @@
## shinan-api
```bash
corepack enable pnpm
pnpm install
```
for start built version
```bash
pnpm build
pnpm start
```
start development version
```bash
pnpm dev
```
+8
View File
@@ -0,0 +1,8 @@
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"subject-case": [2, "always", ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case", "camel-case"]],
"type-enum": [2, "always", ["add", "build", "chore", "update", "docs", "feat", "fix", "refactor", "revert", "style", "test", "sample"]],
"scope-enum": [1, "always", ["common", "core", "app", "testing", "bug"]],
},
};
View File
+10
View File
@@ -0,0 +1,10 @@
{
"watch": ["src", ".env"],
"ext": "ts,json",
"ignore": ["dist"],
"events": {
"start": "clear"
},
"exec": "npm run build && node --trace-warnings dist/server.js",
"delay": 500
}
+104
View File
@@ -0,0 +1,104 @@
{
"name": "shinan_api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"prrebuild": "rimraf dist",
"build": "tsc -p tsconfig.build.json",
"typeCheck": "tsc --noEmit",
"start": "NODE_ENV=production node --trace-warnings dist/server.js",
"dev": "nodemon",
"tsx": "tsx watch --env-file=.env src/server.ts",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"",
"seed": "node dist/db/seeders/index.js",
"seed:ts": "ts-node src/db/seeders/index.ts",
"test": "jest --config jest.config.js --coverage",
"prepare": "husky || true"
},
"lint-staged": {
"**/*.ts": [
"pnpm run format",
"pnpm run lint:fix",
"git add ."
]
},
"keywords": [],
"author": "Mahyar",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "^3.691.0",
"@faker-js/faker": "^7.6.0",
"@nestjs/mapped-types": "^2.0.6",
"ansi-colors": "^4.1.3",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"bullmq": "^5.25.6",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
"csv": "^6.3.11",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.1",
"handlebars": "^4.7.8",
"inversify": "^6.1.4",
"inversify-express-utils": "6.4.6",
"ioredis": "^5.4.1",
"jalali-moment": "^3.3.11",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.8.1",
"mongoose-sequence": "^6.0.1",
"morgan": "^1.10.0",
"multer": "1.4.5-lts.1",
"multer-s3": "^3.0.1",
"node-cache": "^5.1.2",
"nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"slugify": "^1.6.6",
"socket.io": "^4.8.1",
"strip-ansi": "6.0.1",
"swagger-ui-express": "^5.0.1",
"winston": "^3.17.0"
},
"devDependencies": {
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@types/bcrypt": "^5.0.2",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/mongoose-sequence": "^3.0.11",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.12",
"@types/multer-s3": "^3.0.3",
"@types/node": "^20.17.6",
"@types/nodemailer": "^6.4.16",
"@types/passport": "^1.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/swagger-ui-express": "^4.1.7",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"nodemon": "^3.1.7",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
},
"packageManager": "pnpm@9.13.0+sha512.beb9e2a803db336c10c9af682b58ad7181ca0fbd0d4119f2b33d5f2582e96d6c0d93c85b23869295b765170fbdaa92890c0da6ada457415039769edf3c959efe"
}
+7708
View File
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
import { AsyncContainerModule } from "inversify";
import { IOCTYPES } from "./ioc.types";
import { AboutUsRepo, CreateAboutUsRepo } from "../modules/about-us/aboutUs.repository";
import { AboutUsService } from "../modules/about-us/aboutUs.service";
import {
AddressRepo,
CityRepo,
ProvinceRepo,
createAddressRepo,
createCityRepo,
createProvinceRepo,
} from "../modules/address/address.repository";
import { AddressService } from "../modules/address/address.service";
import { AdminService } from "../modules/admin/admin.service";
import { AdminRepository, createAdminRepository } from "../modules/admin/repository/admin";
import { ContractRepository, createContractRepository } from "../modules/admin/repository/contract";
import { BannerRepository, SliderRepository, createBannerRepo, createSliderRepo } from "../modules/admin/repository/media";
import { PermissionRepository, createPermissionRepo } from "../modules/admin/repository/permission";
import { RoleRepository, createRoleRepo } from "../modules/admin/repository/role";
import { AuthService } from "../modules/auth/auth.service";
import { BlogService } from "../modules/blog/blog.service";
import { BlogRepository, createBlogRepo } from "../modules/blog/repository/blog";
import { BlogCategoryRepo, createBlogCategoryRepo } from "../modules/blog/repository/blogCategory";
import { BrandRepository, createBrandRepository } from "../modules/brand/brand.repository";
import { BrandService } from "../modules/brand/brand.service";
import {
CancelOrderItemRepo,
CancelOrderRepo,
CancelReasonRepo,
createCancelOrderItemRepo,
createCancelOrderRepo,
createCancelReasonRepo,
} from "../modules/cancel/cancel.repository";
import { CancelService } from "../modules/cancel/cancel.service";
import { CartRepository, CartShipItemRepo, createCartRepo, createCartShipItemRepo } from "../modules/cart/cart.repository";
import { CartService } from "../modules/cart/cart.service";
import {
AttributeValueRepo,
CategoryAttributeRepo,
CategoryRepository,
ColorRepository,
MeterageRepository,
SizeRepository,
createAttributeValueRepo,
createCategoryAttributeRepo,
createCategoryRepo,
createColorRepo,
createMeterageRepo,
createSizeRepo,
} from "../modules/category/category.repository";
import { CategoryService } from "../modules/category/category.service";
import { ChatGateway } from "../modules/chat/chat.gateway";
import { ChatService } from "../modules/chat/chat.service";
import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository";
import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/repository/message.repository";
import { WsAuthService } from "../modules/chat/wsAuth.service";
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
import { ContactUsService } from "../modules/contact-us/contactUs.service";
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
import { CouponService } from "../modules/Coupon/coupon.service";
import { FaqRepo, createFaqRepo } from "../modules/faq/faq.repository";
import { FaqService } from "../modules/faq/faq.service";
import { FineService } from "../modules/fine/fine.service";
import { FineRepo, createFineRepo } from "../modules/fine/repository/fine.repository";
import { FineRuleRepo, createFineRuleRepo } from "../modules/fine/repository/fineRule.repository";
import { AsanPardakhtGateway } from "../modules/IPG/gateways/asanpardakht";
import { ZarinPalGateway } from "../modules/IPG/gateways/zarinpal";
import { PaymentGateway } from "../modules/IPG/PaymentGateway";
import { JobService } from "../modules/job/job.service";
import { JobRepo, createJobRepo } from "../modules/job/repository/job.repo";
import { ResumeRepo, createResumeRepo } from "../modules/job/repository/resume.repo";
import { LandingService } from "../modules/landing/landing.service";
import { LearningService } from "../modules/learning/learning.service";
import { CreateLearningRepo, LearningRepo } from "../modules/learning/repository/learning";
import { CreateLearningCategoryRepo, LearningCategoryRepo } from "../modules/learning/repository/learningCategory";
import { CreateLearningProgressRepo, LearningProgressRepo } from "../modules/learning/repository/learningProgress";
import { MediaService } from "../modules/media/media.service";
import { CreateNewsletterRepo, NewsletterRepo } from "../modules/newsletter/newsletter.repository";
import { NewsletterService } from "../modules/newsletter/newsletter.service";
import { NotificationRepo, createNotificationRepo } from "../modules/notification/notification.repository";
import { NotificationService } from "../modules/notification/notification.service";
import { OrderItemRepo, OrderRepository, createOrderItemRepo, createOrderRepo } from "../modules/order/order.repository";
import { OrderService } from "../modules/order/order.service";
import {
CartPaymentRepository,
PRPaymentRepository,
PaymentMethodRepo,
createCartPaymentRepo,
createPRPaymentRepo,
createPaymentMethodRepo,
} from "../modules/payment/payment.repository";
import { PaymentService } from "../modules/payment/providers/payment.service";
import { PRPaymentService } from "../modules/payment/providers/PR-payment.service";
import { PricingRepository, createPricingRepo } from "../modules/pricing/pricing.repository";
import { PricingService } from "../modules/pricing/pricing.service";
import { ProductRequestService } from "../modules/product/providers/product-request.service";
import { ProductService } from "../modules/product/providers/product.service";
import { CommentRepository, createCommentRepo } from "../modules/product/Repository/comment";
import { IncredibleOffersRepo, createIncredibleOffersRepo } from "../modules/product/Repository/incredibleOffers";
import { PopularProductRepo, createPopularProductRepo } from "../modules/product/Repository/popularProduct";
import { PriceHistoryRepo, createPriceHistoryRepo } from "../modules/product/Repository/pricehistory";
import { ProductRepository, createProductRepo } from "../modules/product/Repository/product";
import { ProductAdRepo, createProductAdRepo } from "../modules/product/Repository/productAd";
import { ProductObserveRepo, createProductObserveRepo } from "../modules/product/Repository/productObserve";
import { ProductReportRepo, createProductReportRepo } from "../modules/product/Repository/productReport";
import { ProductRequestRepo, createProductRequestRepo } from "../modules/product/Repository/productRequest";
import { ProductVariantRepository, createProductVariantRepo } from "../modules/product/Repository/productVarinat";
import { QuestionRepository, createQuestionRepo } from "../modules/product/Repository/question";
import { ReportQuestionRepo, createReportQuestionRepo } from "../modules/product/Repository/reportQuestion";
import {
ReturnOrderItemRepo,
ReturnOrderRepo,
ReturnReasonRepo,
createReturnOrderItemRepo,
createReturnOrderRepo,
createReturnReasonRepo,
} from "../modules/return/return.repository";
import { ReturnService } from "../modules/return/return.service";
import { DocumentTypeRepo, createDocumentTypeRepo } from "../modules/seller/repository/documentType.repository";
import { LegalSellerRepo, createLegalSellerRepo } from "../modules/seller/repository/legalSeller.repository";
import { RealSellerRepo, createRealSellerRepo } from "../modules/seller/repository/realSeller.repository";
import { SellerContractRepo, createSellerContractRepo } from "../modules/seller/repository/sellerContract.repository";
import { SellerDocumentRepo, createSellerDocumentRepo } from "../modules/seller/repository/sellerDocument.repository";
import { SellerStatusRepo, createSellerStatusRepo } from "../modules/seller/repository/sellerStatus.repository";
import { WholesaleRequestRepo, createWholesaleRequestRepo } from "../modules/seller/repository/wholesaleRequest.repositroy";
import { SellerRepository, createSellerRepository } from "../modules/seller/seller.repository";
import { SellerService } from "../modules/seller/seller.service";
import { ShipmentRepository, createShipmentRepository } from "../modules/shipment/shipment.repository";
import { ShipmentService } from "../modules/shipment/shipment.service";
import { ShopRepo, createShopRepo } from "../modules/shop/shop.repository";
import { ShopService } from "../modules/shop/shop.service";
import { CreateSiteSettingRepo, SiteSettingRepo } from "../modules/site-setting/siteSetting.repository";
import { SiteSettingService } from "../modules/site-setting/siteSetting.service";
import { TicketRepository, createTicketRepo } from "../modules/ticket/repository/ticket";
import { TicketCategoryRepository, createTicketCategoryRepo } from "../modules/ticket/repository/ticketCategory";
import { TicketMessageRepository, createTicketMessageRepo } from "../modules/ticket/repository/ticketMessage";
import { TicketService } from "../modules/ticket/ticket.service";
import { TokenRepository, createTokenRepository } from "../modules/token/token.repository";
import { TokenService } from "../modules/token/token.service";
import { UserRepository, WishlistRepo, createUserRepository, createWishlistRepo } from "../modules/user/user.repository";
import { UserService } from "../modules/user/user.service";
import { TransactionRepo, createTransactionRepo } from "../modules/wallet/repository/transaction.repo";
import { WalletRepo, createWalletRepo } from "../modules/wallet/repository/wallet.repo";
import { WithdrawalRepo, createWithdrawalRepo } from "../modules/wallet/repository/withdrawal.repo";
import { WalletService } from "../modules/wallet/wallet.service";
import { WarrantyRepository, createWarrantyRepository } from "../modules/warranty/warranty.repository";
import { WarrantyService } from "../modules/warranty/warranty.service";
import { CacheService } from "../utils/cache.service";
import { PassportAuth } from "../utils/passport.service";
import { RedisService } from "../utils/redis.service";
const containerModules = new AsyncContainerModule(async (bind) => {
bind(IOCTYPES.PassportAuth).to(PassportAuth).inSingletonScope();
// bind(IOCTYPES.PassportAuth).to(PassportAuth).inSingletonScope();
bind<PaymentGateway>(IOCTYPES.PaymentGateway).to(PaymentGateway).inSingletonScope();
bind<ZarinPalGateway>(IOCTYPES.ZarinPalGateway).to(ZarinPalGateway).inSingletonScope();
bind<AsanPardakhtGateway>(IOCTYPES.AsanPardakhtGateway).to(AsanPardakhtGateway).inSingletonScope();
// #region services
bind<CategoryService>(IOCTYPES.CategoryService).to(CategoryService).inSingletonScope();
bind<AuthService>(IOCTYPES.AuthService).to(AuthService).inSingletonScope();
bind<ChatGateway>(IOCTYPES.ChatGateway).to(ChatGateway).inSingletonScope();
bind<ChatService>(IOCTYPES.ChatService).to(ChatService).inSingletonScope();
bind<WsAuthService>(IOCTYPES.WsAuthService).to(WsAuthService).inSingletonScope();
bind<TokenService>(IOCTYPES.TokenService).to(TokenService).inSingletonScope();
bind<SellerService>(IOCTYPES.SellerService).to(SellerService).inSingletonScope();
bind<UserService>(IOCTYPES.UserService).to(UserService).inSingletonScope();
bind<AdminService>(IOCTYPES.AdminService).to(AdminService).inSingletonScope();
bind<ProductService>(IOCTYPES.ProductService).to(ProductService).inSingletonScope();
bind<ProductRequestService>(IOCTYPES.ProductRequestService).to(ProductRequestService).inSingletonScope();
bind<BrandService>(IOCTYPES.BrandService).to(BrandService).inSingletonScope();
bind<WarrantyService>(IOCTYPES.WarrantyService).to(WarrantyService).inSingletonScope();
bind<ShipmentService>(IOCTYPES.ShipmentService).to(ShipmentService).inSingletonScope();
bind<CartService>(IOCTYPES.CartService).to(CartService).inSingletonScope();
bind<PaymentService>(IOCTYPES.PaymentService).to(PaymentService).inSingletonScope();
bind<PRPaymentService>(IOCTYPES.PRPaymentService).to(PRPaymentService).inSingletonScope();
bind<OrderService>(IOCTYPES.OrderService).to(OrderService).inSingletonScope();
bind<CouponService>(IOCTYPES.CouponService).to(CouponService).inSingletonScope();
bind<LandingService>(IOCTYPES.LandingService).to(LandingService).inSingletonScope();
bind<BlogService>(IOCTYPES.BlogService).to(BlogService).inSingletonScope();
bind<TicketService>(IOCTYPES.TicketService).to(TicketService).inSingletonScope();
bind<WalletService>(IOCTYPES.WalletService).to(WalletService).inSingletonScope();
bind<JobService>(IOCTYPES.JobService).to(JobService).inSingletonScope();
bind<FaqService>(IOCTYPES.FaqService).to(FaqService).inSingletonScope();
bind<SiteSettingService>(IOCTYPES.SiteSettingService).to(SiteSettingService).inSingletonScope();
bind<NotificationService>(IOCTYPES.NotificationService).to(NotificationService).inSingletonScope();
bind<ReturnService>(IOCTYPES.ReturnService).to(ReturnService).inSingletonScope();
bind<CancelService>(IOCTYPES.CancelService).to(CancelService).inSingletonScope();
bind<FineService>(IOCTYPES.FineService).to(FineService).inSingletonScope();
bind<AddressService>(IOCTYPES.AddressService).to(AddressService).inSingletonScope();
bind<ShopService>(IOCTYPES.ShopService).to(ShopService).inSingletonScope();
bind<MediaService>(IOCTYPES.MediaService).to(MediaService).inSingletonScope();
bind<ContactUsService>(IOCTYPES.ContactUsService).to(ContactUsService).inSingletonScope();
bind<AboutUsService>(IOCTYPES.AboutUsService).to(AboutUsService).inSingletonScope();
bind<NewsletterService>(IOCTYPES.NewsletterService).to(NewsletterService).inSingletonScope();
bind<PricingService>(IOCTYPES.PricingService).to(PricingService).inSingletonScope();
bind<LearningService>(IOCTYPES.LearningService).to(LearningService).inSingletonScope();
bind<RedisService>(IOCTYPES.RedisService).to(RedisService).inSingletonScope();
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
// #endregion
// #region repository
bind<CategoryRepository>(IOCTYPES.CategoryRepository).toDynamicValue(createCategoryRepo).inSingletonScope();
bind<CategoryAttributeRepo>(IOCTYPES.CategoryAttributeRepository).toDynamicValue(createCategoryAttributeRepo).inSingletonScope();
bind<AttributeValueRepo>(IOCTYPES.AttributeValueRepository).toDynamicValue(createAttributeValueRepo).inSingletonScope();
bind<TokenRepository>(IOCTYPES.TokenRepository).toDynamicValue(createTokenRepository).inSingletonScope();
bind<UserRepository>(IOCTYPES.UserRepository).toDynamicValue(createUserRepository).inSingletonScope();
bind<AdminRepository>(IOCTYPES.AdminRepository).toDynamicValue(createAdminRepository).inSingletonScope();
bind<SellerRepository>(IOCTYPES.SellerRepository).toDynamicValue(createSellerRepository).inSingletonScope();
bind<LegalSellerRepo>(IOCTYPES.LegalSellerRepo).toDynamicValue(createLegalSellerRepo).inSingletonScope();
bind<RealSellerRepo>(IOCTYPES.RealSellerRepo).toDynamicValue(createRealSellerRepo).inSingletonScope();
bind<SellerDocumentRepo>(IOCTYPES.SellerDocumentRepo).toDynamicValue(createSellerDocumentRepo).inSingletonScope();
bind<SellerContractRepo>(IOCTYPES.SellerContractRepo).toDynamicValue(createSellerContractRepo).inSingletonScope();
bind<SellerStatusRepo>(IOCTYPES.SellerStatusRepo).toDynamicValue(createSellerStatusRepo).inSingletonScope();
bind<DocumentTypeRepo>(IOCTYPES.DocumentTypeRepo).toDynamicValue(createDocumentTypeRepo).inSingletonScope();
bind<BrandRepository>(IOCTYPES.BrandRepository).toDynamicValue(createBrandRepository).inSingletonScope();
bind<WarrantyRepository>(IOCTYPES.WarrantyRepository).toDynamicValue(createWarrantyRepository).inSingletonScope();
bind<ProductRepository>(IOCTYPES.ProductRepository).toDynamicValue(createProductRepo).inSingletonScope();
bind<ShipmentRepository>(IOCTYPES.ShipmentRepository).toDynamicValue(createShipmentRepository).inSingletonScope();
bind<ProductVariantRepository>(IOCTYPES.ProductVariantRepository).toDynamicValue(createProductVariantRepo).inSingletonScope();
bind<ColorRepository>(IOCTYPES.ColorRepository).toDynamicValue(createColorRepo).inSingletonScope();
bind<SizeRepository>(IOCTYPES.SizeRepository).toDynamicValue(createSizeRepo).inSingletonScope();
bind<MeterageRepository>(IOCTYPES.MeterageRepository).toDynamicValue(createMeterageRepo).inSingletonScope();
bind<QuestionRepository>(IOCTYPES.QuestionRepository).toDynamicValue(createQuestionRepo).inSingletonScope();
bind<CommentRepository>(IOCTYPES.CommentRepository).toDynamicValue(createCommentRepo).inSingletonScope();
bind<CartRepository>(IOCTYPES.CartRepository).toDynamicValue(createCartRepo).inSingletonScope();
bind<CartPaymentRepository>(IOCTYPES.CartPaymentRepo).toDynamicValue(createCartPaymentRepo).inSingletonScope();
bind<PRPaymentRepository>(IOCTYPES.PRPaymentRepo).toDynamicValue(createPRPaymentRepo).inSingletonScope();
bind<OrderRepository>(IOCTYPES.OrderRepository).toDynamicValue(createOrderRepo).inSingletonScope();
bind<OrderItemRepo>(IOCTYPES.OrderItemRepo).toDynamicValue(createOrderItemRepo).inSingletonScope();
bind<ProductRequestRepo>(IOCTYPES.ProductRequestRepo).toDynamicValue(createProductRequestRepo).inSingletonScope();
bind<PricingRepository>(IOCTYPES.PricingRepo).toDynamicValue(createPricingRepo).inSingletonScope();
bind<CartShipItemRepo>(IOCTYPES.CartShipItemRepo).toDynamicValue(createCartShipItemRepo).inSingletonScope();
bind<PaymentMethodRepo>(IOCTYPES.PaymentMethodRepo).toDynamicValue(createPaymentMethodRepo).inSingletonScope();
bind<CouponRepo>(IOCTYPES.CouponRepo).toDynamicValue(createCouponRepo).inSingletonScope();
bind<CouponUsageRepo>(IOCTYPES.CouponUsageRepo).toDynamicValue(createCouponUsageRepo).inSingletonScope();
bind<SliderRepository>(IOCTYPES.SliderRepository).toDynamicValue(createSliderRepo).inSingletonScope();
bind<BannerRepository>(IOCTYPES.BannerRepository).toDynamicValue(createBannerRepo).inSingletonScope();
bind<ReportQuestionRepo>(IOCTYPES.ReportQuestionRepo).toDynamicValue(createReportQuestionRepo).inSingletonScope();
bind<ContractRepository>(IOCTYPES.ContractRepository).toDynamicValue(createContractRepository).inSingletonScope();
bind<ProductReportRepo>(IOCTYPES.ProductReportRepo).toDynamicValue(createProductReportRepo).inSingletonScope();
bind<PriceHistoryRepo>(IOCTYPES.PriceHistoryRepo).toDynamicValue(createPriceHistoryRepo).inSingletonScope();
bind<ProductAdRepo>(IOCTYPES.ProductAdRepo).toDynamicValue(createProductAdRepo).inSingletonScope();
bind<PopularProductRepo>(IOCTYPES.PopularProductRepo).toDynamicValue(createPopularProductRepo).inSingletonScope();
bind<IncredibleOffersRepo>(IOCTYPES.IncredibleOffersRepo).toDynamicValue(createIncredibleOffersRepo).inSingletonScope();
bind<ProductObserveRepo>(IOCTYPES.ProductObserveRepo).toDynamicValue(createProductObserveRepo).inSingletonScope();
bind<BlogRepository>(IOCTYPES.BlogRepository).toDynamicValue(createBlogRepo).inSingletonScope();
bind<BlogCategoryRepo>(IOCTYPES.BlogCategoryRepo).toDynamicValue(createBlogCategoryRepo).inSingletonScope();
bind<LearningRepo>(IOCTYPES.LearningRepo).toDynamicValue(CreateLearningRepo).inSingletonScope();
bind<LearningProgressRepo>(IOCTYPES.LearningProgressRepo).toDynamicValue(CreateLearningProgressRepo).inSingletonScope();
bind<LearningCategoryRepo>(IOCTYPES.LearningCategoryRepo).toDynamicValue(CreateLearningCategoryRepo).inSingletonScope();
bind<RoleRepository>(IOCTYPES.RoleRepository).toDynamicValue(createRoleRepo).inSingletonScope();
bind<PermissionRepository>(IOCTYPES.PermissionRepository).toDynamicValue(createPermissionRepo).inSingletonScope();
bind<TicketRepository>(IOCTYPES.TicketRepository).toDynamicValue(createTicketRepo).inSingletonScope();
bind<TicketMessageRepository>(IOCTYPES.TicketMessageRepository).toDynamicValue(createTicketMessageRepo).inSingletonScope();
bind<TicketCategoryRepository>(IOCTYPES.TicketCategoryRepository).toDynamicValue(createTicketCategoryRepo).inSingletonScope();
bind<ChatRepo>(IOCTYPES.ChatRepository).toDynamicValue(createChatRepo).inSingletonScope();
bind<ChatMessageRepo>(IOCTYPES.ChatMessageRepository).toDynamicValue(createChatMessageRepo).inSingletonScope();
bind<WalletRepo>(IOCTYPES.WalletRepo).toDynamicValue(createWalletRepo).inSingletonScope();
bind<TransactionRepo>(IOCTYPES.TransactionRepo).toDynamicValue(createTransactionRepo).inSingletonScope();
bind<WithdrawalRepo>(IOCTYPES.WithdrawalRepo).toDynamicValue(createWithdrawalRepo).inSingletonScope();
bind<WishlistRepo>(IOCTYPES.WishlistRepo).toDynamicValue(createWishlistRepo).inSingletonScope();
bind<JobRepo>(IOCTYPES.JobRepo).toDynamicValue(createJobRepo).inSingletonScope();
bind<ResumeRepo>(IOCTYPES.ResumeRepo).toDynamicValue(createResumeRepo).inSingletonScope();
bind<FaqRepo>(IOCTYPES.FaqRepo).toDynamicValue(createFaqRepo).inSingletonScope();
bind<NotificationRepo>(IOCTYPES.NotificationRepo).toDynamicValue(createNotificationRepo).inSingletonScope();
bind<ReturnOrderRepo>(IOCTYPES.ReturnOrderRepo).toDynamicValue(createReturnOrderRepo).inSingletonScope();
bind<CancelOrderRepo>(IOCTYPES.CancelOrderRepo).toDynamicValue(createCancelOrderRepo).inSingletonScope();
bind<ReturnOrderItemRepo>(IOCTYPES.ReturnOrderItemRepo).toDynamicValue(createReturnOrderItemRepo).inSingletonScope();
bind<CancelOrderItemRepo>(IOCTYPES.CancelOrderItemRepo).toDynamicValue(createCancelOrderItemRepo).inSingletonScope();
bind<ReturnReasonRepo>(IOCTYPES.ReturnReasonRepo).toDynamicValue(createReturnReasonRepo).inSingletonScope();
bind<CancelReasonRepo>(IOCTYPES.CancelReasonRepo).toDynamicValue(createCancelReasonRepo).inSingletonScope();
bind<FineRuleRepo>(IOCTYPES.FineRuleRepo).toDynamicValue(createFineRuleRepo).inSingletonScope();
bind<FineRepo>(IOCTYPES.FineRepo).toDynamicValue(createFineRepo).inSingletonScope();
bind<ShopRepo>(IOCTYPES.ShopRepo).toDynamicValue(createShopRepo).inSingletonScope();
bind<WholesaleRequestRepo>(IOCTYPES.WholesaleRequestRepo).toDynamicValue(createWholesaleRequestRepo).inSingletonScope();
bind<AddressRepo>(IOCTYPES.AddressRepo).toDynamicValue(createAddressRepo).inSingletonScope();
bind<CityRepo>(IOCTYPES.CityRepo).toDynamicValue(createCityRepo).inSingletonScope();
bind<ProvinceRepo>(IOCTYPES.ProvinceRepo).toDynamicValue(createProvinceRepo).inSingletonScope();
bind<ContactUsRepo>(IOCTYPES.ContactUsRepo).toDynamicValue(CreateContactUsRepo).inSingletonScope();
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
// #endregion
});
export { containerModules };
+136
View File
@@ -0,0 +1,136 @@
export const IOCTYPES = {
//
CustomDecoratorMiddleware: Symbol.for("CustomDecoratorMiddleware"),
// #region service
PassportAuth: Symbol.for("PassportAuth"),
AuthService: Symbol.for("AuthService"),
CategoryService: Symbol.for("CategoryService"),
UserService: Symbol.for("UserService"),
AdminService: Symbol.for("AdminService"),
SellerService: Symbol.for("SellerService"),
SellerStatusRepo: Symbol.for("SellerStatusRepo"),
ProductService: Symbol.for("ProductService"),
SiteSettingService: Symbol.for("SiteSettingService"),
ProductRequestService: Symbol.for("ProductRequestService"),
ShipmentService: Symbol.for("ShipmentService"),
BrandService: Symbol.for("BrandService"),
WarrantyService: Symbol.for("WarrantyService"),
CacheService: Symbol.for("CacheService"),
RedisService: Symbol.for("RedisService"),
TokenService: Symbol.for("TokenService"),
CartService: Symbol.for("CartService"),
PaymentService: Symbol.for("PaymentService"),
PRPaymentService: Symbol.for("PRPaymentService"),
OrderService: Symbol.for("OrderService"),
CouponService: Symbol.for("CouponService"),
LandingService: Symbol.for("LandingService"),
BlogService: Symbol.for("BlogService"),
LearningService: Symbol.for("LearningService"),
TicketService: Symbol.for("TicketService"),
ChatGateway: Symbol.for("ChatGateway"),
ChatService: Symbol.for("ChatService"),
WsAuthService: Symbol.for("WsAuthService"),
WalletService: Symbol.for("WalletService"),
JobService: Symbol.for("JobService"),
FaqService: Symbol.for("FaqService"),
NotificationService: Symbol.for("NotificationService"),
ReturnService: Symbol.for("ReturnService"),
CancelService: Symbol.for("CancelService"),
FineService: Symbol.for("FineService"),
AddressService: Symbol.for("AddressService"),
ShopService: Symbol.for("ShopService"),
MediaService: Symbol.for("MediaService"),
ContactUsService: Symbol.for("ContactUsService"),
AboutUsService: Symbol.for("AboutUsService"),
NewsletterService: Symbol.for("NewsletterService"),
PricingService: Symbol.for("PricingService"),
OrderQueue: Symbol.for("OrderQueue"),
// #endregion
// #region repository
CategoryRepository: Symbol.for("CategoryRepository"),
CategoryAttributeRepository: Symbol.for("CategoryAttributeRepository"),
AttributeValueRepository: Symbol.for("AttributeValueRepository"),
UserRepository: Symbol.for("UserRepository"),
AdminRepository: Symbol.for("AdminRepository"),
SellerRepository: Symbol.for("SellerRepository"),
LegalSellerRepo: Symbol.for("LegalSellerRepo"),
RealSellerRepo: Symbol.for("RealSellerRepo"),
SellerContractRepo: Symbol.for("SellerContractRepo"),
SellerDocumentRepo: Symbol.for("SellerDocumentRepo"),
SellerFinancialRepo: Symbol.for("SellerFinancialRepo"),
DocumentTypeRepo: Symbol.for("DocumentTypeRepo"),
ProductRepository: Symbol.for("ProductRepository"),
ProductVariantRepository: Symbol.for("ProductVariantRepository"),
BrandRepository: Symbol.for("BrandRepository"),
WarrantyRepository: Symbol.for("WarrantyRepository"),
ShipmentRepository: Symbol.for("ShipmentRepository"),
TokenRepository: Symbol.for("TokenRepository"),
ColorRepository: Symbol.for("ColorRepository"),
SizeRepository: Symbol.for("SizeRepository"),
MeterageRepository: Symbol.for("MeterageRepository"),
QuestionRepository: Symbol.for("QuestionRepository"),
CommentRepository: Symbol.for("CommentRepository"),
ProductRequestRepo: Symbol.for("ProductRequestRepo"),
CartRepository: Symbol.for("CartRepository"),
CartShipItemRepo: Symbol.for("CartShipItemRepo"),
CartPaymentRepo: Symbol.for("CartPaymentRepo"),
PricingRepo: Symbol.for("PricingRepo"),
PRPaymentRepo: Symbol.for("PRPaymentRepo"),
PaymentMethodRepo: Symbol.for("PaymentMethodRepo"),
OrderRepository: Symbol.for("OrderRepository"),
OrderItemRepo: Symbol.for("OrderItemRepo"),
CouponRepo: Symbol.for("CouponRepo"),
CouponUsageRepo: Symbol.for("CouponUsageRepo"),
SliderRepository: Symbol.for("SliderRepository"),
BannerRepository: Symbol.for("BannerRepository"),
ProductReportRepo: Symbol.for("ProductReportRepo"),
ReportQuestionRepo: Symbol.for("ReportQuestionRepo"),
PriceHistoryRepo: Symbol.for("PriceHistoryRepo"),
ProductAdRepo: Symbol.for("ProductAdRepo"),
PopularProductRepo: Symbol.for("PopularProductRepo"),
IncredibleOffersRepo: Symbol.for("IncredibleOffersRepo"),
ProductObserveRepo: Symbol.for("ProductObserveRepo"),
BlogRepository: Symbol.for("BlogRepository"),
BlogCategoryRepo: Symbol.for("BlogCategoryRepo"),
LearningRepo: Symbol.for("LearningRepo"),
LearningProgressRepo: Symbol.for("LearningProgressRepo"),
LearningCategoryRepo: Symbol.for("LearningCategoryRepo"),
RoleRepository: Symbol.for("RoleRepository"),
PermissionRepository: Symbol.for("PermissionRepository"),
TicketRepository: Symbol.for("TicketRepository"),
TicketMessageRepository: Symbol.for("TicketMessageRepository"),
TicketCategoryRepository: Symbol.for("TicketCategoryRepository"),
ChatRepository: Symbol.for("ChatRepository"),
ChatMessageRepository: Symbol.for("ChatMessageRepository"),
WalletRepo: Symbol.for("WalletRepo"),
TransactionRepo: Symbol.for("TransactionRepo"),
WithdrawalRepo: Symbol.for("WithdrawalRepo"),
WishlistRepo: Symbol.for("WishlistRepo"),
JobRepo: Symbol.for("JobRepo"),
ResumeRepo: Symbol.for("ResumeRepo"),
FaqRepo: Symbol.for("FaqRepo"),
SiteSettingRepo: Symbol.for("SiteSettingRepo"),
NotificationRepo: Symbol.for("NotificationRepo"),
ReturnOrderRepo: Symbol.for("ReturnOrderRepo"),
CancelOrderRepo: Symbol.for("CancelOrderRepo"),
ContractRepository: Symbol.for("ContractRepo"),
ReturnOrderItemRepo: Symbol.for("ReturnOrderItemRepo"),
CancelOrderItemRepo: Symbol.for("CancelOrderItemRepo"),
ReturnReasonRepo: Symbol.for("ReturnReasonRepo"),
CancelReasonRepo: Symbol.for("CancelReasonRepo"),
FineRuleRepo: Symbol.for("FineRuleRepo"),
FineRepo: Symbol.for("FineRepo"),
ShopRepo: Symbol.for("ShopRepo"),
WholesaleRequestRepo: Symbol.for("WholesaleRequestRepo"),
AddressRepo: Symbol.for("AddressRepo"),
CityRepo: Symbol.for("CityRepo"),
ProvinceRepo: Symbol.for("ProvinceRepo"),
ContactUsRepo: Symbol.for("ContactUsRepo"),
AboutUsRepo: Symbol.for("AboutUsRepo"),
NewsletterRepo: Symbol.for("NewsletterRepo"),
// #endregion
Logger: Symbol.for("Logger"),
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
AsanPardakhtGateway: Symbol.for("AsanPardakhtGateway"),
PaymentGateway: Symbol.for("PaymentGateway"),
};
+159
View File
@@ -0,0 +1,159 @@
import compression from "compression";
import cors from "cors";
import express, { Application } from "express";
import { Container } from "inversify";
import { InversifyExpressServer } from "inversify-express-utils";
import morgan from "morgan";
import { errorHandler, lastHandler, notFoundHandler } from "./core/app/app.errorHandler";
import { appConfig } from "./core/config/app.config";
import { Logger } from "./core/logging/logger";
import { customMorganFormat, stream } from "./core/logging/morgan";
import { containerModules } from "./IOC/ioc.config";
import { IOCTYPES } from "./IOC/ioc.types";
import { PassportAuth } from "./utils/passport.service";
import { setupSwagger } from "./utils/swagger";
//controller
import "./modules/admin";
import "./modules/brand/brand.controller";
import "./modules/warranty/warranty.controller";
import "./modules/shipment/shipment.controller";
import "./modules/category/category.controller";
import "./modules/product/product.controller";
import "./modules/seller/seller.controller";
import "./modules/user/user.controller";
import "./modules/cart/cart.controller";
import "./modules/Coupon/coupon.controller";
import "./modules/order/order.controller";
import "./modules/return/return.controller";
import "./modules/blog/blog.controller";
import "./modules/landing/landing.controller";
import "./modules/ticket/ticket.controller";
import "./modules/chat/chat.controller";
import "./modules/job/job.controller";
import "./modules/faq/faq.controller";
import "./modules/notification/notification.controller";
import "./modules/wallet/wallet.controller";
import "./modules/payment/payment.controller";
import "./modules/address/address.controller";
import "./modules/newsletter/newsletter.controller";
import "./modules/contact-us/contactUs.controller";
import "./modules/cancel/cancel.controller";
import "./modules/about-us/aboutUs.controller";
import "./modules/learning/learning.controller";
import "./modules/fine/fine.controller";
import "./modules/media/media.controller";
import "./modules/site-setting/siteSetting.controller";
import "./modules/shop/shop.controller";
import "./modules/ping";
class App {
private readonly _container: Container;
// private readonly _port: number;
private logger = new Logger();
constructor(container: Container) {
this._container = container;
// this._port = port;
this.loadModules();
}
private async loadModules() {
await this._container.loadAsync(containerModules);
}
public Build(): Application {
const server = new InversifyExpressServer(this._container, null /*,{ rootPath: "/api" }*/);
return server
.setConfig(async (app) => {
await this.setMiddleware(app);
})
.setErrorConfig((app) => {
this.catchError(app);
})
.build();
// .listen(this._port, () => {
// this.logger.info(`listening on http://localhost:${this._port}`);
// });
}
private async setMiddleware(app: Application) {
//remove x-powered-by header from response
app.disable("x-powered-by");
this.logger.debug("disable the default x header of express");
/**
* trust for reverse proxy to get correct ip address of user
* Configure based on deployment environment
*/
const trustedProxies = process.env.TRUSTED_PROXIES;
const trustProxyHops = process.env.TRUST_PROXY_HOPS;
if (trustedProxies === "true") {
// recommended for production with multiple proxies/CDNs
app.set("trust proxy", 1);
this.logger.debug("trust proxy enabled for first hop only (production with CDN/proxies)");
} else if (trustProxyHops && !isNaN(parseInt(trustProxyHops))) {
// Trust specific number of proxy hops
const hops = parseInt(trustProxyHops);
app.set("trust proxy", hops);
this.logger.debug(`trust proxy enabled for ${hops} hops`);
} else if (trustedProxies && trustedProxies !== "false") {
// Trust specific proxy IPs (comma-separated list)
const proxyList = trustedProxies.split(",").map((ip) => ip.trim());
app.set("trust proxy", proxyList);
this.logger.debug(`trust proxy enabled for specific IPs: ${trustedProxies}`);
} else if (process.env.NODE_ENV === "development") {
// For development, allow localhost proxies
app.set("trust proxy", "loopback");
this.logger.debug("trust proxy enabled for development (loopback only)");
} else {
// Production without reverse proxy - don't trust any proxies
app.set("trust proxy", false);
this.logger.debug("trust proxy disabled for security");
}
// app.get("/ip", (request, response) => response.send(request.ip));
/**
* compress the response
*/
app.use(compression());
/**
* configure body parser
*/
app.use(express.json());
/**
* configure morgan for http logging
*/
app.use(morgan(customMorganFormat, { stream }));
/**
* configure cors
*/
app.use(cors(appConfig.cors));
/**
* config passport
*/
const passportAuth = this._container.get<PassportAuth>(IOCTYPES.PassportAuth);
app.use(passportAuth.initialize());
passportAuth.plug();
/**
* swagger document
*/
setupSwagger(app);
this.logger.debug("setup swagger docs");
}
private catchError(app: Application) {
app.use(notFoundHandler());
app.use(errorHandler());
app.use(lastHandler());
}
}
export { App };
+51
View File
@@ -0,0 +1,51 @@
import { BaseHttpController } from "inversify-express-utils";
import { HttpStatus } from "../enums/httpStatus.enum";
import { ResponseFactory } from "../factories/response.factory";
import { IPageFormat } from "../interfaces/PageFormatInterface";
import { IPaginateDataFormat } from "../interfaces/PaginateDataFormatInterface";
abstract class BaseController extends BaseHttpController {
private formatLink(page: number | boolean, limit: number): string | boolean {
const { protocol, hostname, path } = this.httpContext.request;
if (!page) return false;
return `${protocol}://${hostname}${path}?page=${page}&limit=${limit}`;
}
protected formatPage(page: number, limit: number, totalItems: number): IPageFormat {
const totalPages = Math.ceil(totalItems / limit);
const prevPage = page === 1 ? false : page - 1;
const nextPage = page >= totalPages ? false : page + 1;
return {
page,
limit,
totalItems,
totalPages,
prevPage: this.formatLink(prevPage, limit),
nextPage: this.formatLink(nextPage, limit),
};
}
protected paginate(count: number): IPaginateDataFormat {
const page = this.httpContext.request.query["page"] as string;
const limit = this.httpContext.request.query["limit"] as string;
const paginationInfo = this.formatPage(parseInt(page) || 1, parseInt(limit) || 10, count);
const results: IPaginateDataFormat = {
pager: paginationInfo,
};
return results;
}
protected response(data: Record<string, unknown>, status: HttpStatus = HttpStatus.Ok) {
const response = ResponseFactory.successResponse(status, data);
return this.json(response, status);
}
}
export { BaseController };
+23
View File
@@ -0,0 +1,23 @@
import { Model } from "mongoose";
abstract class BaseRepository<T> {
public model: Model<T>;
protected constructor(model: Model<T>) {
this.model = model;
}
async findById(id: string | number) {
return this.model.findById(id);
}
async findAll() {
return this.model.find().sort({ createdAt: -1 });
}
async delete(id: string) {
return this.model.findByIdAndDelete(id);
}
}
export { BaseRepository };
+19
View File
@@ -0,0 +1,19 @@
import { interfaces } from "inversify-express-utils";
const USER_KEY = "custom:user";
// Middleware to resolve parameters for custom decorators
export function resolveCustomDecorators(httpContext: interfaces.HttpContext, target: any, propertyKey: string | symbol, args: any[]) {
const metadata = Reflect.getMetadata(USER_KEY, target, propertyKey) || [];
for (const { parameterIndex, callback } of metadata) {
args[parameterIndex] = callback(httpContext);
}
}
export function createParamDecorator(callback: (httpContext: interfaces.HttpContext) => unknown): ParameterDecorator {
return (target, propertyKey, parameterIndex) => {
const existingMetadata = Reflect.getMetadata(USER_KEY, target, propertyKey as any) || [];
existingMetadata.push({ parameterIndex, callback });
Reflect.defineMetadata(USER_KEY, existingMetadata, target, propertyKey as any);
};
}
+91
View File
@@ -0,0 +1,91 @@
import "reflect-metadata";
// ApiOperation decorator
export function ApiOperation(summary: string): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:operation:summary", summary, target, propertyKey);
};
}
// ApiResponse decorator
export function ApiResponse(description: string, status: number = 200, example?: any): MethodDecorator {
return function (target, propertyKey) {
const existingResponses = Reflect.getMetadata("api:responses", target, propertyKey) || [];
existingResponses.push({ description, status, example });
Reflect.defineMetadata("api:responses", existingResponses, target, propertyKey);
};
}
// ApiTags decorator
export function ApiTags(...tags: string[]): ClassDecorator {
return function (target) {
Reflect.defineMetadata("api:tags", tags, target);
};
}
// ApiParam decorator
export function ApiParam(name: string, description: string, required: boolean = false): MethodDecorator {
return function (target, propertyKey) {
const existingParams = Reflect.getMetadata("api:params", target, propertyKey) || [];
existingParams.push({ name, description, required });
Reflect.defineMetadata("api:params", existingParams, target, propertyKey);
};
}
// ApiQuery decorator
export function ApiQuery(
name: string,
description: string,
required: boolean = false,
type: "string" | "array" = "string",
): MethodDecorator {
return function (target, propertyKey) {
const existingQueries = Reflect.getMetadata("api:queries", target, propertyKey) || [];
existingQueries.push({ name, description, required, type });
Reflect.defineMetadata("api:queries", existingQueries, target, propertyKey);
};
}
// ApiBody decorator
export function ApiBody(description: string): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:body", description, target, propertyKey);
};
}
export function ApiModel(dto: any): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:body:dto", dto, target, propertyKey);
};
}
export interface ApiPropertyOptions {
type: string;
description?: string;
example?: any;
required?: boolean;
}
export function ApiProperty(options: ApiPropertyOptions): PropertyDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:property", options, target, propertyKey);
};
}
export function ApiAuth(): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:auth", true, target, propertyKey);
};
}
export function ApiFile(description: string = "file", required: boolean = true, multiple: boolean = false): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:file", { description, required, multiple }, target, propertyKey);
};
}
export function AdminRoute(): MethodDecorator {
return function (target, propertyKey) {
Reflect.defineMetadata("api:isAdmin", true, target, propertyKey);
};
}
+9
View File
@@ -0,0 +1,9 @@
import "reflect-metadata";
import { HttpContext } from "inversify-express-utils";
import { createParamDecorator } from "./param.decorator";
// export function User(): ParameterDecorator {
// return createParamDecorator((httpContext: interfaces.HttpContext) => httpContext.user);
// }
export const User = () => createParamDecorator((ctx: HttpContext) => ctx.request.user);
@@ -0,0 +1,128 @@
import { ValidationArguments, ValidationOptions, registerDecorator } from "class-validator";
import moment from "jalali-moment";
import { Model, isValidObjectId } from "mongoose";
import { CommonMessage } from "../enums/message.enum";
// @ValidatorConstraint({ async: true })
// export class IsTypeValidConstraint implements ValidatorConstraintInterface {
// // eslint-disable-next-line no-unused-vars
// async validate(type: string, _args: ValidationArguments) {
// return type === process.env.USER_TYPE || type === process.env.SELLER_TYPE;
// }
// // eslint-disable-next-line no-unused-vars
// defaultMessage(_args: ValidationArguments) {
// return AuthMessage.TypeError;
// }
// }
// export function IsTypeValid(validationOptions?: ValidationOptions) {
// return function (object: object, propertyName: string) {
// registerDecorator({
// target: object.constructor,
// propertyName: propertyName,
// options: validationOptions,
// constraints: [],
// validator: IsTypeValidConstraint,
// });
// };
// }
// export function IsPhoneExists<T>(model: Model<T>, userId?: string, validationOptions?: ValidationOptions) {
// return function (object: object, propertyName: string) {
// registerDecorator({
// name: "IsPhoneExists",
// target: object.constructor,
// propertyName: propertyName,
// options: validationOptions,
// constraints: [userId],
// validator: {
// async validate(phoneNumber: string, args: ValidationArguments) {
// const [userId] = args.constraints;
// const user = await model.exists({ phoneNumber, _id: { $ne: userId } });
// return !user; // if user exists with a different ID, return false (validation fails)
// },
// // eslint-disable-next-line no-unused-vars
// defaultMessage(_args: ValidationArguments) {
// return AuthMessage.NumberExists;
// },
// },
// });
// };
// }
// export function IsEmailExists<T>(model: Model<T>, userId?: string, validationOptions?: ValidationOptions) {
// return function (object: object, propertyName: string) {
// registerDecorator({
// name: "IsEmailExists",
// target: object.constructor,
// propertyName: propertyName,
// options: validationOptions,
// constraints: [userId],
// validator: {
// async validate(email: string, args: ValidationArguments) {
// const [userId] = args.constraints;
// const user = await model.exists({ email, _id: { $ne: userId } });
// return !user; // if user exists with a different ID, return false (validation fails)
// },
// // eslint-disable-next-line no-unused-vars
// defaultMessage(_args: ValidationArguments) {
// return AuthMessage.EmailExists;
// },
// },
// });
// };
// }
export function IsValidId(model: Model<any> | Array<Model<any>>, validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: "IsValidId",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [model],
validator: {
// eslint-disable-next-line no-unused-vars
async validate(id: string | number, _args: ValidationArguments) {
if (typeof id === "string") {
if (!isValidObjectId(id)) return false;
}
// Normalize to an array for uniform processing
const models = Array.isArray(model) ? model : [model];
// Check the ID in each model
for (const m of models) {
const existId = await m.exists({ _id: id });
if (existId) return true; // ID exists in one of the models
}
return false; // ID does not exist in any of the models
},
defaultMessage(args: ValidationArguments) {
return `${CommonMessage.NotValidId} ${args.property}`;
},
},
});
};
}
export function IsValidPersianDate(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: "IsValidPersianDate",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: {
validate(date: string) {
return moment(date, "jYYYY/jMM/jDD", true).isValid();
},
defaultMessage() {
return "Invalid Persian date format. Expected format is YYYY/MM/DD.";
},
},
});
};
}
+19
View File
@@ -0,0 +1,19 @@
import { Expose } from "class-transformer";
import { IsInt, IsNotEmpty, IsOptional, Max, Min } from "class-validator";
export class PaginationDTO {
@Expose()
@IsOptional()
@IsNotEmpty()
@IsInt()
@Min(1)
page?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsInt()
@Min(1)
@Max(50)
limit?: number;
}
+11
View File
@@ -0,0 +1,11 @@
import { Expose } from "class-transformer";
import { IsMongoId, IsNotEmpty } from "class-validator";
import { CommonMessage } from "../enums/message.enum";
export class ParamDto {
@Expose()
@IsNotEmpty()
@IsMongoId({ message: CommonMessage.NotValidId })
id: string;
}
+14
View File
@@ -0,0 +1,14 @@
export enum CategoryThemeEnum {
Sized = "Size",
Colored = "Color",
Meterage = "Meterage",
No_color_No_sized = "noColor_noSize",
}
export enum categoryAttType {
Text = "text",
Select = "select",
Number = "number",
CheckBox = "checkbox",
// Input = "input",
}
+7
View File
@@ -0,0 +1,7 @@
export enum HttpMethod {
Post = "POST",
Get = "GET",
Patch = "PATCH",
Put = "PUT",
Delete = "DELETE",
}
+13
View File
@@ -0,0 +1,13 @@
export const enum HttpStatus {
Ok = 200,
Created = 201,
Accepted = 202,
NoContent = 204,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
Conflict = 409,
PayloadTooLarge = 413,
InternalServerError = 500,
}
+9
View File
@@ -0,0 +1,9 @@
export enum DisplayLocations {
Mobile = "mobile",
Desktop = "desktop",
}
export enum MediaType {
Banner = "banner",
Slider = "slider",
}
+325
View File
@@ -0,0 +1,325 @@
export const enum AuthMessage {
//otp
OtpSentToNo = "کد به شماره موبایل شما ارسال شد",
OtpNotEmpty = "کد نباید خالی باشد",
OtpAlreadySentToNo = "کد به شماره موبایل شما ارسال شده است",
OtpAlreadySentToEmail = "کد به ایمیل شما ارسال شده است لطفا ایمیل خود را چک کنید یا بعد از چند دقیقه مجدد تلاش کنید",
OtpSentToEmail = "کد به ایمیل شما ارسال شد",
EmailSent = "ایمیل فعال سازی برای شما ارسال شد",
OtpIncorrect = "کد صحیح نمی باشد",
OtpCodeLength = "طول کد باید حداقل ۵ و حداکثر ۵ کاراکتر باشد",
//login/logout/auth
SuccessLogin = "با موفقیت وارد شدید",
PasswordChangedSuccessfully = "پسورد با موفقیت تغییر کرد",
EmailOrPasswordInc = "ایمیل یا پسورد اشتباه است",
ShopNotFound = "فروشگاه با این ایدی پیدا نشد",
IncorrectNumber = "فرمت موبایل اشتباه است",
UserIsLoggedIn = "شما قبلا وارد شدید",
LoggedOut = "شما از سیستم خارج شدید",
NeedLogin = "لطفا وارد حساب کاربری خود شوید",
EmailOrPhone = "یکی از ایمیل یا موبایل باید ارسال شود",
NumberNotEmpty = "شماره موبایل نباید خالی باشد",
TypeNotEmpty = "نوع کاربر نباید خالی باشد",
IncorrectEmail = "فرمت ایمیل اشتباه است",
EmailNotEmpty = "ایمیل نباید خالی باشد",
PasswordNotEmpty = "پسورد نباید خالی باشد",
NumberExists = "این شماره قبلا استفاده شده است",
EmailExists = "این ایمیل قبلا استفاده شده است",
TypeError = "تایپ اشتباه می باشد",
//token
TokenNotEmpty = "توکن نباید خالی باشد",
TokenNotFound = "توکن پیدا نشد",
TokenExpired = " توکن منقضی شده است لطفا دوباره وارد شوید",
TokenNotAssigned = " توکن به هیج کاربری متعلق نمی باشد دوباره وارد شوید",
AuthFailed = "کاربر احراز هویت نشده است",
InsufficientRole = "رول کاربر مجاز نیست",
InsufficientPermissions = "دسترسی‌های کاربر کافی نیست",
}
export const enum SellerMessage {
ShopUpdated = "اطلاعات فروشگاه با موفقیت آپدیت شد",
ShopIdIncorrect = "فرمت شناسه ملی شرکت اشتباه است باید 11 رقم و بدون علامت اضافی و به صورت رشته ارسال شود",
SellerNotFound = "فروشنده یافت نشد",
VideoWasWached = "قبلا این ویدیو را مشاهده کرده اید",
CardNumberIncorrect = "فرمت شماره کارت اشتباه است باید 16 رقم و بدون علامت اضافی و به صورت رشته ارسال شود",
CompanyDuplicate = "مشخصات شرکت تکراری می باشد",
RealSellerDuplicate = "مشخصات فروشنده تکراری می باشد",
SellerDocumentNotFound = "مدرک فروشنده یافت نشد",
DocumentTypeDuplicate = "نوع مدرک تکراری می باشد",
WholesaleRequestPending = "درخواست عمده فروشی شما در حال بررسی می باشد",
WholesaleRequestCreated = "درخواست عمده فروشی با موفقیت ثبت شد",
StatusNotFound = "وضعیت فروشنده یافت نشد",
WholesaleRequestNotFound = "درخواست عمده فروشی یافت نشد",
WholesaleRequestApproved = "درخواست عمده فروشی تایید شد",
InvalidBusinessType = "نوع فعالیت اشتباه است",
}
export const enum UserMessage {
UserUpdated = "اطلاعات کاربر با موفقیت آپدیت شد",
EmailNotExist = "ایمیل کاربر ثبت نشده است",
UserNotFound = "کاربری با این ایدی یافت نشد",
}
export const enum CommonMessage {
NotFoundBySlug = "با این اسلاگ پیدا نشد",
Created = "با موفقیت اضافه شد",
NameNotEmpty = "اسم نباید خالی باشد",
NationalCodeIncorrect = " فرمت کد ملی اشتباه است باید 10 رقم و بدون علامت اضافی و به صورت رشته ارسال شود",
PostalIncorrect = "فرمت شماره پستی اشتباه است باید 10 رقم و بدون علامت اضافی و به صورت رشته ارسال شود",
ShebaIncorrect = "فرمت شماره شبا اشتباه است باید 24 رقم و بدون علامت اضافی و به صورت رشته ارسال شود",
TelephoneNumberIncorrect = "فرمت شماره تلفن اشتباه است نباید شامل کاراکتر های خاص باشد",
NotFound = "کاربری با این ایدی پیدا نشد",
NotValid = "آیدی کاربر صحیح نمی باشد",
NotValidId = "آیدی صحیح نمی باشد",
NotEnoughVariant = "محصول تنوعی ندارد",
NotFoundById = "دیتا با این آیدی یافت نشد",
Deleted = "با موفقیت حذف شد",
Updated = "با موفقیت آپدیت شد",
DuplicateName = "نام تکراری می باشد",
SellerNotFound = "فروشنده یافت نشد",
SettingExists = "تنظیمات وجود دارد.در صورت نیاز آنرا ویرایش کنید.",
}
export const enum CategoryMessage {
Created = "دسته بندی با موفقیت ساخته شد",
Deleted = "دسته بندی با موفقیت حذف شد.",
Updated = "دسته بندی با موفقیت اپدیت شد.",
ParentNotExist = "کتگوری والد با این ایدی وجود ندارد",
description = "توضیحات را وارد کنید",
DuplicateTitle = "عنوان انگلیسی تکراری می باشد",
DuplicateSlug = "عنوان اسلاگ تکراری می باشد",
AlreadyExist = "تنوع قبلا ساخته شده است",
AttAlreadyExist = "ویژگی‌های این دسته بندی قبلا ساخته شده است",
VariantCreated = "تنوع‌های دسته بندی با موفقیت ساخته شدند",
AttributeCreated = "ویژگی‌های دسته بندی با موفقیت ساخته شدند",
NotValidId = "آیدی کتگوری صحیح نمی باشد",
MissingColors = "داده‌های رنگ برای تم انتخاب شده موجود نیست.",
MissingSizes = "داده‌های سایز برای تم انتخاب شده موجود نیست.",
MissingMeterages = "داده‌های متراژ برای تم انتخاب شده موجود نیست.",
InvalidTheme = "ویژگی‌های ارسال شده با تم انتخاب شده مطابقت ندارند یا ناقص هستند.",
NotFound = "کتگوری با این نام پیدا نشد",
ParentHasChild = "فرزند این دسته بندی قبلا ساخته شده است",
CanNotDelete = "دسته بندی نمیتواند حذف شود",
CategoryHasProduct = "این دسته بندی دارای محصول می باشد و قابل حذف نمی باشد",
CategoryIsParent = "این دسته بندی دارای زیر دسته می باشد و قابل حذف نمی باشد",
CategoryNotValidToChose = "کتگوری برای انتخاب مجاز نمی‌باشد",
AttributeDeleted = "ویژگی با موفقیت حذف شد",
}
export const enum BrandMessage {
NotFound = "برندی با این اسم یافت نشد",
Exist = "برند با این نام وجود دارد",
Created = "برند جدید با موفقیت ساخته شد",
}
export const enum ProductMessage {
Created = "کالا با موفقیت ساخته شد",
ProductRequested = "درخواست اضافه کردن کالا با موفقیت ثبت شد",
NotFound = "کالایی با این ایدی یافت نشد",
CreatedAttribute = "مشخصات کالا با موفقیت ساخته شد",
DuplicateName = "نام مدل تکراری می باشد",
ProductNotFound = "محصول با این ایدی یافت نشد",
ProductVariantNotFound = "تنوع محصول با این ایدی یافت نشد",
CanNotUpdate = "این کالا متعلق به فروشنده نمی باشد",
CanNotDeleteQuestion = "حذف پرسش با مشکل مواجه شد.",
CanNotDeleteComment = "حذف کامنت با مشکل مواجه شد.",
ProductReadyForRelease = "کالا ثبت شد و آماده‌ی انتشار می باشد",
ProductIsInNextStep = "کالا قبلا این مرحله را گذرانده است",
ProductIsInDraftStep = "کالا در حالت انتشار نمی‌باشد",
VariantCreated = "تنوع با موفقیت ساخته شد",
VariantUpdated = "تنوع با موفقیت آپدیت شد",
Deleted = "محصول و تمامی تنوع های آن با موفقیت حذف شدند.",
CanNotDelete = "محصول قابل حذف نمی باشد",
CanNotUpdateProduct = "محصول قابل اپدیت نمی باشد",
ProductMustBeInDraft = "کالا برای اپدیت باید در حالت پیش نویس باشد",
ProductUpdated = "محصول با موفقیت آپدیت شد",
ProductWithNoVariantCategoryTheme = "کالا‌هایی که دسته بندی آن ها بدون تنوع می باشد بیشتر از یک تنوع نمی توانند داشته باشند",
Cloned = "کالای مورد نظر کلون شد",
VariantAlreadyExists = "تنوع محصول قبلا ساخته شده است",
}
export const enum ProductRequestMessage {
RequestPhotoInvalid = "برای درج درخواست عکاسی باید تعداد عکس هم ارسال شود",
PhotoShouldSent = "درصورت دارا بودن عکس باید آن را ارسال کنید",
NotFound = "درخواستی برای این فروشنده با این ایدی یافت نشد",
}
export const enum WarrantyMessage {
Created = "گارانتی با موفقیت ساخته شد",
DuplicateName = "نام تکراری می باشد",
}
export const enum ContractMessage {
Created = "قرارداد با موفقیت ساخته شد",
NotFound = "قراردادی با این نام پیدا نشد",
AlreadyExist = "قرارداد قبلا ساخته شده است",
ContractNotFound = "قراردادی پیدا نشد",
ContractAlreadySigned = "قرارداد قبلا امضا شده است",
}
export const enum AttributeMessage {
AttributeNotFound = "ویژگی دسته‌بندی یافت نشد.",
AttributeIdIsIncorrect = "آیدی ویژگی صحیح نمی باشد.",
ColorNotAllowedForSizedTheme = "استفاده از ویژگی رنگ یا متراژ برای دسته‌بندی با تم سایز مجاز نیست.",
SizeNotAllowedForColoredTheme = "استفاده از ویژگی سایز یا متراژ برای دسته‌بندی با تم رنگ مجاز نیست.",
ColorRequiredForColoredTheme = "ویژگی رنگ برای دسته‌بندی با تم رنگ الزامی است.",
SizeRequiredForSizedTheme = "ویژگی سایز برای دسته‌بندی با تم سایز الزامی است.",
MeterageRequiredForMeterageTheme = "ویژگی متراژ برای دسته‌بندی با تم متراژ الزامی است.",
ColorOrSizeNotAllowedForMeterageTheme = "استفاده از ویژگی رنگ یا سایز برای دسته‌بندی با تم متراژ مجاز نیست.",
UnknownCategoryTheme = "تم دسته‌بندی نامشخص است.",
ThemeIsNoColorNoSize = "تم دسته‌بندی بدون رنگ و سایز می‌باشد.",
ThemeIsNoColorNoSizeNoMeterage = "تم دسته‌بندی بدون رنگ، سایز و متراژ می‌باشد.",
}
export const enum CartMessage {
StockNotEnough = "مقدار درخواستی از موجودی کالا بیشتر است",
CartNotFound = "سبد خرید برای این کاربر یافت نشد",
OrderLimitExceeded = "مقدار درخواستی از حد سفارش کالا بیشتر است",
CartIdInvalid = "سبد خرید با این آیدی یافت نشد",
ItemNotFound = "این کالا در سبد خرید موجود نیست",
ItemAdded = "کالا با موفقیت به سبد خرید اضافه شد",
ItemDeleted = "کالا با موفقیت از سبد خرید حذف شد",
CartUpdated = "سبد خرید با موفقیت بروز شد",
ProductNotBelongToVariant = "این تنوع مربوط به این کالا نیست",
CartShipmentItemsNotFound = "آیتم های سبد خرید یافت نشد",
InvalidCartShipmentItem = "آیتم سبد خرید معتبر نیست",
}
export const enum PaymentMessage {
PaymentNotFound = "پرداخت با این شناسه پیدا نشد",
PaymentMethodNotFound = "درگاه پرداخت با این شناسه پیدا نشد",
PaymentMethodsNotFound = "درگاه پرداختی پیدا نشد",
PaymentNotSuccessful = "پرداخت موفقیت آمیز نبوده‌ است",
PaymentSuccessful = "پرداخت آمیز با موفقیت انجام شد",
PRPaymentDesc = "پرداخت برای درخواست ثبت کالا",
CartPaymentDesc = "خرید کالا:",
PaymentNotBelongToUser = "پرداخت برای این کاربر نمی‌باشد",
PaymentRequestFailed = "درخواست پرداخت موفقیت آمیز نبود",
}
export const enum CouponMessage {
NotFound = "کد تخفیف اشتباه است یا غیرفعال شده است",
Expired = "کد تخفیف منقضی شده است",
AmountIsNotSatisfied = "مبلغ خرید برای اعمال کد تخفیف کافی نیست",
UsageLimit = "تعداد استفاده از کد تخفیف به اتمام رسیده است",
CouponUsedBefore = " شما قبلا از این کد تخفیف استفاده کرده‌اید",
CouponAdded = "کد تخفیف اعمال شد",
CouponAlreadyInCart = "کد تخفیف فقط یکبار می‌تواند در هر سبد خرید استفاده شود",
InvalidId = "آیدی کد تخفیف صحیح نمی باشد",
DiscountAmountOrPercentageRequired = "باید یکی از مقادیر مبلغ تخفیف یا درصد را وارد کنید",
BothDiscountOrPercentageShouldNotSend = "نباید هر دو مقدار مبلغ تخفیف و درصد را وارد کنید",
UserCouponUsageLimit = "تعداد استفاده شما از کد تخفیف به اتمام رسیده است",
InvalidProduct = "این کد تخفیف برای این کالا معتبر نمی‌باشد",
InvalidCategory = "این کد تخفیف برای این دسته بندی معتبر نمی‌باشد",
BothIncludedAndExcludedProductsShouldNotSend = "نباید هر دو مقدار محصولات شامل و محصولات مستثنی را وارد کنید",
}
export const enum MediaMessage {
MediaCreated = "مدیا ساخته شد",
SliderCreated = "اسلایدر ساخته شد",
BannerCreated = "بنر ساخته شد",
Activated = "مدیا فعال شد",
UploadLimitSize = "حجم فایل باید کمتر از 6 مگابایت باشد",
}
export const enum SetShipmentMessage {
CartNotFound = "سبد خرید یافت نشد",
InvalidShipmentInfoLength = "باید برای همه اقلام موجود در سبد خرید ارسال‌کننده مشخص کنید",
ProductVariantNotFound = "تنوع محصول یافت نشد",
ShipperNotAvailableForSeller = "این ارسال‌کننده برای این فروشنده موجود نیست",
ShipmentProviderNotFound = "ارائه‌دهنده خدمات ارسال یافت نشد",
CartItemNotFound = "کالای مربوط به این نوع محصول در سبد خرید یافت نشد",
InvalidShipmentItems = "برخی از تنوع‌های ارسال شده معتبر نیستند",
InvalidShipmentSellers = "برای این خرید همچین فروشنده‌ای وجود ندارد",
ShipmentOptionsUnavailable = "روش ارسال برای این کالا موجود نیست",
ShipmentMethodAlreadyActive = "این روش ارسال قبلا انتخاب شده است",
ShipmentMethodNotActive = "این روش ارسال انتخاب نشده است",
}
export const enum AdMessage {
NotFoundWithId = "تبلیغی با این پیدا نشد",
}
export const enum OrderMessage {
NotFound = "سفارشی با این آیدی پیدا نشد",
TrackingDetailInserted = "مشخصات مرسوله با موفقیت ثبت شد",
OrderItemsNotFound = "آیتم های این سفارش یافت نشد",
ShipmentItemNotFound = "آیتم های ارسالی سفارش یافت نشد",
ReturnOrderNotFound = "سفارش برگشتی با این آیدی پیدا نشد",
ReturnOrderItemsNotFound = "آیتم های سفارش برگشتی پیدا نشد",
ReturnApproved = "مرجوعی کالا تایید شد",
ReturnRejected = "مرجوعی کالا رد شد",
ReturnCompleted = "فرایند مرجوعی کامل شد",
ItemReceived = "وضعیت سفارش شما برای این فروشنده به دریافت شده تغییر پیدا کرد",
TrackingCodeNotFound = "برای سفارش شما کد پیگیری یافت نشد",
TrackingEmailSent = "ایمیل حاوی اطلاعات سفارس و کد رهگیری برای شما ارسال شد",
ItemAlreadyReceived = "سفارش شما قبلا تحویل گرفته شده است",
ItemNotShipped = "سفارش هنوز ارسال نشده است",
}
export const enum TicketMessage {
TicketNotBelongToSeller = "این تیکت متعلق به فروشنده نمی‌باشد",
TicketNotFound = "تیکتی با این آیدی پیدا نشد",
}
export const enum ChatMessage {
ChatNotFound = "چتی با این آیدی پیدا نشد",
}
export const enum WalletMessage {
WalletNotFound = "کیف پول فروشنده یافت نشد",
CreditedSuccessfully = "کیف پول با موفقیت شارژ شد",
InsufficientBalance = "موجودی کیف پول کافی نمی‌باشد",
InsufficientBalanceForWithdrawal = "موجودی کیف پول برای برداشت کافی نمی‌باشد",
DebitedSuccessfully = "برداشت از کیف پول با موفقیت انجام شد",
WithdrawalNotFound = "درخواست برداشتی با این آیدی یافت نشد",
WithdrawalFailed = "برداشت از کیف پول با مشکل مواجه شد",
WithdrawalSubmitted = "درخواست برداشت از کیف پول ثبت شد",
CreditFailed = "شارژ کیف پول ناموفق بود",
InvalidTransaction = "تراکنش نامعتبر است",
RejectWithdrawalRequest = "درخواست برداشت از کیف پول رد شد",
}
export const enum ShopMessage {
ShopNotFound = "فروشگاه برای این فروشنده یافت نشد",
ShopNotFoundById = "با این ایدی فروشگاه یافت نشد",
ShopNameExist = "نام فروشگاه تکراری می باشد",
NotFound = "فروشگاه یافت نشد",
}
export const enum AddressMessage {
Updated = "آدرس با موفقیت آپدیت شد",
NotFound = "آدرس کاربر یافت نشد",
UserShouldHaveAddress = "برای ثبت سفارش .کاربر ملزم به ثبت آدرس می‌باشد",
InvalidLocation = "مختصات جغرافیایی اشتباه می باشد",
SellerAddressNotFound = "برای ثبت قرار داد ابتدا آدرس خود در قسمت پروفابل ثبت کنید",
}
export const enum PricingMessage {
NotFound = "قیمت ها بافت نشد",
PhotographyFee = "هزینه عکاسی",
InsertFeeNotFound = "هزینه درج محصول یافت نشد",
UnboxingVideoFee = "هزینه ویدیو انباکسینگ یافت نشد",
ExpertReviewsFeeNotFound = "هزینه نقد و بررسی یافت نشد",
expertReviewsFee = "هزینه نقد و بررسی",
TypeIsExist = "این نوع قبلا ثبت شده است",
}
export const enum AdminMessage {
FullNameNotEmpty = "نام و نام خانوادگی نباید خالی باشد",
UserNameNotEmpty = "نام کاربری نباید خالی باشد",
RoleNotEmpty = "نقش کاربر نباید خالی باشد",
PasswordNotEmpty = "پسورد نباید خالی باشد",
AdminIsExist = "این نام کاربری قبلا ثبت شده است",
RoleNotFound = "نقش ادمین یافت نشد",
EmailNotEmpty = "ایمیل نباید خالی باشد",
PhoneNotEmpty = "شماره موبایل نباید خالی باشد",
DeletedSuccessfully = "حذف با موفقیت انجام شد",
NotFound = "ادمین یافت نشد",
}
export const enum RoleMessage {
RoleNotFound = "نقش یافت نشد",
RoleNameNotEmpty = "نام نقش نباید خالی باشد",
RoleNameDuplicate = "نام نقش تکراری می باشد",
PermissionsNotEmpty = "دسترسی‌ها نباید خالی باشد",
RoleExist = "این نقش قبلا ثبت شده است",
}
+22
View File
@@ -0,0 +1,22 @@
export enum PaymentStatus {
Pending = "Pending",
Cancelled = "Cancelled",
Completed = "Completed",
}
export enum OrdersStatus {
wait_payment = "wait_payment",
process_by_seller = "process_by_sellers",
cancelled_system = "cancelled_system",
Cancelled = "cancelled",
Delivered = "Delivered",
}
export enum OrderItemsStatus {
Processing = "Processing",
Shipped = "Shipped",
Delivered = "Delivered",
cancelled_shop = "cancelled_shop",
cancelled_system = "cancelled_system",
cancelled_user = "cancelled_user",
Returned = "Returned",
}
+9
View File
@@ -0,0 +1,9 @@
export enum PageEnum {
Faq = "Faq",
Seller = "Seller",
ProductTag = "ProductTag",
ProductDescription = "ProductDescription",
ProductBenefit = "ProductBenefit",
ShipmentProcess = "ShipmentProcess",
Policy = "Policy",
}
+10
View File
@@ -0,0 +1,10 @@
export enum GatewayProvider {
Zarinpal = "Zarinpal",
Asanpardakht = "asanPardakht",
}
export enum PaymentMethodType {
Online = "online",
POS = "pos",
Wallet = "wallet",
}
+28
View File
@@ -0,0 +1,28 @@
export enum ProductMarketStatus {
Out_of_stock = "out_of_stock",
Marketable = "Marketable",
stop_production = "stopProduction",
}
export enum ProductStatus {
Draft = "Draft",
Pending = "Pending",
Approved = "Approved",
Rejected = "Rejected",
}
export enum ProductSource {
LOCAL = "local",
IMPORT = "import",
}
export enum ProductDiscountType {
Fixed = "fixed",
Percent = "percent",
}
export enum CreateProductStep {
Detail = 1,
Attribute,
Image,
}
@@ -0,0 +1,5 @@
export enum Question_CommentStatus {
Pending = "pending",
Accepted = "accepted",
Rejected = "rejected",
}
+9
View File
@@ -0,0 +1,9 @@
export enum SellerType {
WHOLESALER = "WHOLESALER",
RETAILER = "RETAILER",
}
export enum SellerGender {
Male = "MALE",
Female = "FEMALE",
}
+6
View File
@@ -0,0 +1,6 @@
// Enum for Delivery Types
export enum DeliveryType {
SameDay = "SameDay", //(1) day
Express = "Express", //(1-2) days
Standard = "Standard", //(3-5) days
}
+6
View File
@@ -0,0 +1,6 @@
export enum StatusEnum {
Pending = "Pending",
Approved = "Approved",
Completed = "Completed",
Rejected = "Rejected",
}
+24
View File
@@ -0,0 +1,24 @@
import { HttpStatus } from "../enums/httpStatus.enum";
import { IErrorResponse, ISuccessResponse } from "../interfaces/IAppResponse";
export class ResponseFactory {
public static successResponse(status: HttpStatus, data: Record<string, unknown>): ISuccessResponse {
return {
status,
success: true,
results: {
...data,
},
};
}
public static errorResponse(status: HttpStatus, message: string, details: string[] = []): IErrorResponse {
return {
status,
success: false,
error: {
message,
details,
},
};
}
}
+8
View File
@@ -0,0 +1,8 @@
//interfaces
export { IAppOptions } from "./interfaces/IAppOptions";
export { IErrorResponse, ISuccessResponse } from "./interfaces/IAppResponse";
//enums
export { HttpStatus } from "./enums/httpStatus.enum";
export { HttpMethod } from "./enums/httpMethod.enum";
//factories
export { ResponseFactory } from "./factories/response.factory";
+7
View File
@@ -0,0 +1,7 @@
import { CorsOptions } from "cors";
import { Options } from "express-rate-limit";
export interface IAppOptions {
cors: CorsOptions;
rate: Partial<Options>;
}
+16
View File
@@ -0,0 +1,16 @@
import { HttpStatus } from "../enums/httpStatus.enum";
export interface IErrorResponse {
status: HttpStatus;
success: boolean;
error: {
message: string;
details: string[];
};
}
export interface ISuccessResponse {
status: HttpStatus;
success: boolean;
results: Record<string, unknown>;
}
@@ -0,0 +1,8 @@
export interface IPageFormat {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: string | boolean;
nextPage: string | boolean;
}
@@ -0,0 +1,6 @@
import { IPageFormat } from "./PageFormatInterface";
export interface IPaginateDataFormat {
pager: IPageFormat;
// docs: Array<Record<string, unknown>>;
}
+61
View File
@@ -0,0 +1,61 @@
declare namespace NodeJS {
interface ProcessEnv {
PORT: string;
NODE_ENV: string;
MONGO_URI: string;
REDIS_URI: string;
REDIS_HOST: string;
REDIS_PORT: string;
REDIS_PASSWORD: string;
SMS_API_KEY: string;
SMS_SECRET: string;
SMS_NUMBER: string;
SMS_PATTERN_OTP: string;
SMS_PATTERN_USER_ORDER_PEND: string;
SMS_PATTERN_USER_ORDER_CREATE: string;
SMS_PATTERN_USER_ORDER_SHIPPED: string;
SMS_PATTERN_USER_ORDER_RETURNED: string;
SMS_PATTERN_SELLER_ORDER_CREATE: string;
SMS_PATTERN_SELLER_ORDER_CANCELLED: string;
SMTP_EMAIL: string;
SMTP_HOST: string;
SMTP_PORT: string;
SMTPS_PORT: string;
SMTP_SECURE: string;
SMTP_USER: string;
SMTP_PASSWORD: string;
JWT_SECRET: string;
JWT_EXPIRE_ACCESS: string;
JWT_EXPIRE_REFRESH: string;
BUCKET_ACCESS_KEY: string;
BUCKET_SECRET_KEY: string;
BUCKET_NAME: string;
BUCKET_URL: string;
DEFAULT_ADMIN_EMAIL: string;
DEFAULT_ADMIN_PASSWORD: string;
IPG_TYPE: string;
SITE_URL: string;
STORE_URL: string;
SELLER_URL: string;
ZARINPAL_MERCHANT_ID: string;
ASANPARDAKHT_MERCHANT_ID: string;
ASANPARDAKHT_MERCHANT_CONFIG_ID: string;
ASANPARDAKHT_USER: string;
ASANPARDAKHT_PASS: string;
MAP_API_KEY: string;
NESHAN_API_KEY: string;
LOGGER_LEVEL: string;
}
}
+9
View File
@@ -0,0 +1,9 @@
export type AuthTokenPayload = {
sub: string;
};
export type TokenType = "Access" | "Refresh";
export type AuthAdminToken = {
sub: string;
};
+42
View File
@@ -0,0 +1,42 @@
interface MetaData {
mobile?: string;
email?: string;
order_id?: string;
}
export interface ZarinPalPGNewArgs {
merchant_id: string;
amount: number;
description: string;
callback_url: string;
metadata?: MetaData;
currency?: "IRR" | "IRT";
}
interface PaymentRequestData {
code: number;
message: string;
authority: string;
fee_type: string;
fee: number;
}
export interface ZarinPalPGNewRequestData {
data: PaymentRequestData;
error: string[];
}
interface PaymentVerifyData {
code: number;
message: string;
card_hash: string;
card_pan: string;
ref_id: number;
fee_type: string;
fee: number;
}
export interface ZarinPalPGVerifyData {
data: PaymentVerifyData;
error: string[];
}
+102
View File
@@ -0,0 +1,102 @@
import { NotificationType } from "../../modules/notification/models/Abstraction/INotification";
export type CompareSearchQueries = {
limit: number;
page: number;
productId: number;
};
export type VariantQueries = {
page: number;
limit: number;
variantStatus: boolean;
shipmentMethod: number;
stock: boolean;
includeAd: boolean;
specialSale: boolean;
sort: string[];
q: string;
};
export type SellerProductQueries = {
page: number;
limit: number;
categoryId: string;
status: string;
q: string;
};
export type FilterByStatusProductsQueries = {
page: number;
limit: number;
status: string;
};
export type ProductsCommentsQueries = {
page: number;
limit: number;
productId: number;
status: string;
};
export type ProductsQuestionsQueries = {
page: number;
limit: number;
productId: number;
status: string;
};
export type SellerOrdersQueries = {
page: number;
limit: number;
shipperId: string;
status: string;
since: number;
maxPrice: number;
minPrice: number;
};
export type PaymentsQueries = {
page: number;
limit: number;
status: string;
since: number;
maxPrice: number;
minPrice: number;
};
export type AdminContactUsQueries = {
page: number;
limit: number;
email_phone: string;
};
export type AdminReturnsQueries = {
page: number;
limit: number;
};
export type AdminCancelsQueries = {
page: number;
limit: number;
};
export type SellerFinesQueries = {
page: number;
limit: number;
};
export type AdminFinesQueries = {
page: number;
limit: number;
};
export type NotificationQuery = {
page: number;
limit: number;
unread: boolean;
type: NotificationType;
since: number;
};
export type OrderStatusQuery = "Delivered" | "Processing" | "Cancelled";
+16
View File
@@ -0,0 +1,16 @@
// /* eslint-disable no-unused-vars */
// /* eslint-disable @typescript-eslint/no-unused-vars */
// import { ApiError } from "../../core/app/app.errors";
// // import express from "express";
// // import { Express } from "express";
// // import { Request } from "express";
// declare global {
// namespace Express {
// interface authError extends ApiError {}
// interface Request {
// authError?: authError;
// file?: Express.MulterS3.File;
// }
// }
// }
+40
View File
@@ -0,0 +1,40 @@
import { ErrorRequestHandler, RequestHandler } from "express";
import { MongooseError } from "mongoose";
import { ApiError, NotFoundError } from "./app.errors";
import { HttpStatus, IErrorResponse, ResponseFactory } from "../../common";
import { Logger } from "../logging/logger";
const logger = new Logger();
export function notFoundHandler(): RequestHandler {
// eslint-disable-next-line no-unused-vars
return (req, _res, _next) => {
throw new NotFoundError(`can not get ${req.path}`);
};
}
export function errorHandler(): ErrorRequestHandler {
// eslint-disable-next-line no-unused-vars
return (err: Error, req, res, _next) => {
const trace = process.env.NODE_ENV === "development" ? { ...err, stack: err.stack } : { ...err, stack: err.stack };
res.on("finish", () => {
logger.error(`[${req.ip}] - ${req.method} ${req.path} - ${res.statusCode}`, trace);
});
const errCode: HttpStatus = err instanceof ApiError ? err.code : 500;
const errMsg =
err instanceof ApiError ? err.message : err instanceof MongooseError ? err.name : "Something went wrong. Please try again later.";
const errDetails = err instanceof ApiError ? err.details : err instanceof MongooseError ? [err.message] : [];
const errResponse: IErrorResponse = ResponseFactory.errorResponse(errCode, errMsg, errDetails);
res.status(errCode).json(errResponse);
};
}
export function lastHandler(): ErrorRequestHandler {
// eslint-disable-next-line no-unused-vars
return (err: Error, _req, res, _next) => {
logger.error(`Last error catcher: ${err.message}`);
const errResponse: IErrorResponse = ResponseFactory.errorResponse(500, "Something went wrong. Please try again later");
res.status(500).json(errResponse);
};
}
+62
View File
@@ -0,0 +1,62 @@
export class ApiError extends Error {
readonly code: number;
readonly message: string;
readonly details: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(code: number, message: string, details: string[] = [], ...args: any[]) {
super(message, ...args);
this.code = code;
this.message = message;
this.details = details;
}
}
export class BadRequestError extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(400, "Bad request", messageArray);
}
}
export class UnauthorizedError extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(401, "Unauthorized", messageArray);
}
}
export class ForbiddenError extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(403, "Forbidden", messageArray);
}
}
export class NotFoundError extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(404, "NotFound", messageArray);
}
}
export class InternalError extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(500, "something went wrong please try again later", messageArray);
}
}
export class jwtExpiredErr extends ApiError {
constructor(details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(401, "token has been expired!", messageArray);
}
}
export class UploadError extends ApiError {
constructor(fieldName: string, details: string | string[] = []) {
const messageArray = Array.isArray(details) ? details : [details];
super(422, `error uploading file in field ${fieldName}`, messageArray);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { IAppOptions } from "../../common";
export const appConfig: IAppOptions = {
cors: { origin: true, credentials: true, optionsSuccessStatus: 204 },
rate: {
windowMs: 5 * 60 * 1000, // 5 min
message: "Too many requests from this IP, please try again after an hour",
limit: 11,
standardHeaders: true, //rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // disable the `X-RateLimit-*` headers
},
};
+76
View File
@@ -0,0 +1,76 @@
import colors from "ansi-colors";
import { injectable } from "inversify";
import stripAnsi from "strip-ansi";
import winston, { Logger as WinstonLogger } from "winston";
const { combine, timestamp, printf, errors } = winston.format;
const logLevelColor: Record<string, keyof typeof colors> = {
error: "red",
warn: "yellow",
info: "green",
verbose: "cyan",
debug: "blue",
};
const customFormat1 = printf(({ level, message, timestamp, context = "App", ...meta }) => {
const cleanedMessage = stripAnsi(message as string);
return `[${timestamp}] [${context}] [${level.toUpperCase()}]: ${cleanedMessage} ${Object.keys(meta).length ? JSON.stringify(meta, null, 0) : ""}`;
});
const customFormat2 = printf(({ level, message, timestamp, context = "App", trace, ...meta }) => {
const coloredTimestamp = colors.white(`[${timestamp}]`);
const Express = colors.green(`[Express]`);
const logLevel = logLevelColor[level];
const coloredLevel = (colors[logLevel] as (str: string) => string)(`[${level.toUpperCase()}]`);
const coloredMessage = colors.green(message as string);
const coloredContext = colors.yellow(`[${context}]`);
const errorTrace = trace instanceof Error ? trace.stack : trace ? JSON.stringify(trace, null, 4) : "";
const metaLog = Object.keys(meta).length ? JSON.stringify(meta, null, 4) : "";
return `${Express} - ${coloredTimestamp} ${coloredLevel} ${coloredContext} : ${coloredMessage} ${errorTrace} ${metaLog}`;
});
@injectable()
export class Logger {
public logger: WinstonLogger;
constructor(private context: string = "App") {
this.logger = winston.createLogger({
level: process.env.LOGGER_LEVEL || "info",
format: combine(errors({ stack: true }), timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), customFormat1),
transports: [
// new winston.transports.File({ filename: "logs/combined.log", handleExceptions: true }),
// new winston.transports.File({ level: "error", filename: "logs/error.log", handleExceptions: true }),
new winston.transports.Console({
level: "debug",
format: combine(errors({ stack: true }), timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), customFormat2),
// handleExceptions: true,
}),
// new winston.transports.Http({}),
],
exitOnError: false,
});
}
info(message: any, ...meta: any[]) {
this.logger.info({ message, context: this.context, ...meta });
}
error(message: any, trace?: any) {
this.logger.error({ message, trace, context: this.context });
}
warn(message: any, ...meta: any[]) {
this.logger.warn({ message, context: this.context, ...meta });
}
debug(message: any, ...meta: any[]) {
this.logger.debug({ message, context: this.context, ...meta });
}
verbose(message: any, ...meta: any[]) {
this.logger.verbose({ message, context: this.context, ...meta });
}
}
+62
View File
@@ -0,0 +1,62 @@
import ansiColor from "ansi-colors";
import { Request, Response } from "express";
import morgan from "morgan";
import { Logger } from "./logger";
const logger = new Logger("HTTP");
morgan.token("coloredMethod", (req: Request) => {
const method = req.method;
let color;
switch (method) {
case "GET":
color = ansiColor.cyan;
break;
case "POST":
color = ansiColor.blue;
break;
case "PUT":
color = ansiColor.yellow;
break;
case "DELETE":
color = ansiColor.red;
break;
default:
color = ansiColor.white;
break;
}
return color(method);
});
morgan.token("coloredStatus", (_req: Request, res: Response) => {
const status = res.statusCode;
let color;
// Set color based on status code
if (status >= 500) {
color = ansiColor.red;
} else if (status >= 400) {
color = ansiColor.yellow;
} else if (status >= 300) {
color = ansiColor.cyan;
} else if (status >= 200) {
color = ansiColor.magenta;
} else {
color = ansiColor.white;
}
return color(status.toString());
});
const customMorganFormat = ":coloredMethod [:remote-addr] :url :coloredStatus :response-time ms - :user-agent - :res[content-length]";
const stream = {
write: (message: string) => {
logger.info(message.trim());
},
};
export { stream, customMorganFormat };
+131
View File
@@ -0,0 +1,131 @@
import { NextFunction, Request, RequestHandler, Response } from "express";
import { JsonWebTokenError, TokenExpiredError } from "jsonwebtoken";
import passport from "passport";
import { AuthMessage } from "../../common/enums/message.enum";
import { IAdmin } from "../../modules/admin/models/Abstraction/IAdmin";
import { IPermission, PermissionEnum } from "../../modules/admin/models/Abstraction/IPermission";
import { IRole, RoleEnum } from "../../modules/admin/models/Abstraction/IRole";
import { ISeller } from "../../modules/seller/models/Abstraction/ISeller";
import { IUser } from "../../modules/user/models/Abstraction/IUser";
import { ForbiddenError, UnauthorizedError, jwtExpiredErr } from "../app/app.errors";
class Guard {
private static instance: Guard;
private constructor() {}
static get(): Guard {
if (!Guard.instance) {
Guard.instance = new Guard();
}
return Guard.instance;
}
/**
* Check if the admin has the required role
* @param {RoleEnum[]} requiredRoles The role that the admin must have (e.g., [SUPERADMIN, MODERATOR])
*/
public checkAdminRole =
(requiredRoles: RoleEnum[]): RequestHandler =>
(req, _res, next) => {
const admin = req.user as IAdmin;
const adminRole = admin.role as unknown as IRole;
if (!admin.role || !requiredRoles.includes(adminRole.name)) {
return next(new ForbiddenError(AuthMessage.InsufficientRole));
}
next();
};
/**
* Check if the admin has the required permissions
* @param requiredPermissions The list of permissions the admin must have (e.g., [BRAND, CATEGORY])
*/
public checkAdminPermissions =
(requiredPermissions: PermissionEnum[]): RequestHandler =>
(req, _res, next) => {
const admin = req.user as IAdmin;
const adminRole = admin.role as unknown as IRole;
const adminPermissions = admin.permissions as unknown as IPermission[];
const isSuperAdmin = adminRole.name == RoleEnum.SUPERADMIN;
if (!adminRole || (!isSuperAdmin && !adminPermissions)) {
return next(new ForbiddenError(AuthMessage.InsufficientPermissions));
}
const hasAllPermissions = requiredPermissions.every((permission) =>
adminPermissions.some((adminPerm) => adminPerm.name === permission),
);
if (!hasAllPermissions && !isSuperAdmin) {
return next(new ForbiddenError(AuthMessage.InsufficientPermissions));
}
next();
};
/**
* callback function to handle authentication based on the sent jwt token to the endpoint
* @param req
* @param _res
* @param next
*/
public handleJwt =
(req: Request, _res: Response, next: NextFunction) =>
async (err: Error, decoded: IUser | ISeller, info: string | object | JsonWebTokenError | TokenExpiredError): Promise<void> => {
if (err) {
console.log({ err });
return next(new UnauthorizedError(AuthMessage.AuthFailed));
}
if (info instanceof TokenExpiredError) {
return next(new jwtExpiredErr([`${info.expiredAt}`]));
}
if (info instanceof JsonWebTokenError) {
return next(new UnauthorizedError(info.message));
}
if (info) {
console.log({ info });
return next(new UnauthorizedError(AuthMessage.AuthFailed));
}
if (!decoded) {
return next(new UnauthorizedError(AuthMessage.NeedLogin));
}
req.user = decoded;
next();
};
public authUser = (): RequestHandler => async (req, res, next) => {
passport.authenticate("UserJwt", { session: false }, this.handleJwt(req, res, next))(req, res, next);
};
public authSeller = (): RequestHandler => async (req, res, next) => {
passport.authenticate("SellerJwt", { session: false }, this.handleJwt(req, res, next))(req, res, next);
};
public authAdmin = (): RequestHandler => async (req, res, next) => {
passport.authenticate("AdminJwt", { session: false }, this.handleJwt(req, res, next))(req, res, next);
};
public AuthSellerOrAdmin = (): RequestHandler => async (req, res, next) => {
passport.authenticate("SellerOrAdmin", { session: false }, this.handleJwt(req, res, next))(req, res, next);
};
public authOptional = (): RequestHandler => async (req, res, next) => {
passport.authenticate(
"OptionalUserJwt",
{ session: false },
async (err: Error, decoded: IUser, info: string | object | JsonWebTokenError | TokenExpiredError) => {
if (err || info instanceof JsonWebTokenError || info instanceof TokenExpiredError) {
// اگر مشکلی در توکن وجود دارد، درخواست ادامه می‌یابد بدون خطا
return next();
}
if (decoded) {
req.user = decoded; // اگر توکن معتبر است، کاربر را به `req.user` اضافه کنید
}
next();
},
)(req, res, next);
};
}
const instance = Guard.get();
export { instance as Guard };
@@ -0,0 +1,112 @@
import { ClassConstructor, plainToInstance } from "class-transformer";
import { ValidationError, validate } from "class-validator";
import { NextFunction, Request, RequestHandler, Response } from "express";
import { injectable } from "inversify";
import { BadRequestError } from "../app/app.errors";
@injectable()
export class ValidationMiddleware {
static validateInput(DTO: ClassConstructor<any>): RequestHandler {
return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {
try {
// Convert the request body to an instance of the DTO class
const validationClass = plainToInstance(DTO, req.body, {
excludeExtraneousValues: true,
});
// Validate the DTO instance, including nested objects
const errors: ValidationError[] = await validate(validationClass, {
whitelist: true,
// forbidNonWhitelisted: true,
// transform: true,
});
if (errors.length > 0) {
const errorMsg = errors.map((error) => ValidationMiddleware.flattenValidationErrors(error)).flat();
next(new BadRequestError(errorMsg));
// throw new BadRequestError("Bad Request", errorMsg);
} else {
req.body = validationClass;
next();
}
} catch (error) {
next(error);
}
};
}
static validateParameter(DTO: ClassConstructor<any>): RequestHandler {
return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {
try {
// Convert the request body to an instance of the DTO class
const validationClass = plainToInstance(DTO, req.params, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
console.log(validationClass);
// Validate the DTO instance, including nested objects
const errors: ValidationError[] = await validate(validationClass, {
whitelist: true,
// forbidNonWhitelisted: true,
// transform: true,
});
if (errors.length > 0) {
const errorMsg = errors.map((error) => ValidationMiddleware.flattenValidationErrors(error)).flat();
next(new BadRequestError(errorMsg));
// throw new BadRequestError("Bad Request", errorMsg);
} else {
req.query = validationClass;
next();
}
} catch (error) {
next(error);
}
};
}
static validateQuery(DTO: ClassConstructor<any>): RequestHandler {
return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {
try {
// Convert the request body to an instance of the DTO class
const validationClass = plainToInstance(DTO, req.query, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
// Validate the DTO instance, including nested objects
const errors: ValidationError[] = await validate(validationClass, {
whitelist: true,
// forbidNonWhitelisted: true,
// transform: true,
});
if (errors.length > 0) {
const errorMsg = errors.map((error) => ValidationMiddleware.flattenValidationErrors(error)).flat();
next(new BadRequestError(errorMsg));
// throw new BadRequestError("Bad Request", errorMsg);
} else {
req.query = validationClass;
next();
}
} catch (error) {
next(error);
}
};
}
// Helper function to recursively flatten nested validation errors
private static flattenValidationErrors(error: ValidationError): string[] {
if (error.constraints) {
return Object.values(error.constraints);
}
if (error.children && error.children.length > 0) {
return error.children.map(ValidationMiddleware.flattenValidationErrors).flat();
}
return [];
}
}
+43
View File
@@ -0,0 +1,43 @@
import mongoose from "mongoose";
import { Logger } from "../core/logging/logger";
export const clearDatabase = async (logger: Logger) => {
try {
const db = mongoose.connection.db;
if (db) {
// Get all collections
const collections = await db.listCollections().toArray();
collections
.map((collection) => collection.name)
.forEach(async (collectionName) => {
logger.debug(`Dropped collection: ${collectionName}`, "Database");
await db.dropCollection(collectionName);
});
}
} catch (error) {
logger.error(`error happened in drop collections`, error);
}
const collections = mongoose.connection.collections;
for (const key in collections) {
const collection = collections[key];
try {
await collection.drop();
logger.debug(`Dropped collection: ${key}`, "Database");
} catch (error: unknown) {
if (error instanceof Error) {
if (error.message === "ns not found") {
logger.warn(`Collection not found: ${key}`, "Database");
continue;
} else if (error.message.includes("a background operation is currently running")) {
logger.warn(`Skipping collection due to background operation: ${key}`, "Database");
continue;
} else {
throw error;
}
}
}
}
};
+18
View File
@@ -0,0 +1,18 @@
import Redis from "ioredis";
import mongoose from "mongoose";
const MONGO_URI = process.env.MONGO_URI as string;
export const connectMongo = async () => {
await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 2000 });
};
export const connectRedis = () => {
// return new Redis({
// host: process.env.REDIS_HOST,
// port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379,
// password: process.env.REDIS_PASSWORD,
// maxRetriesPerRequest: null,
// });
return new Redis(process.env.REDIS_URI, { maxRetriesPerRequest: null });
};
+33
View File
@@ -0,0 +1,33 @@
[
{ "id": 1, "name": "آذربایجان شرقی" },
{ "id": 2, "name": "آذربایجان غربی" },
{ "id": 3, "name": "اردبیل" },
{ "id": 4, "name": "اصفهان" },
{ "id": 5, "name": "البرز" },
{ "id": 6, "name": "ایلام" },
{ "id": 7, "name": "بوشهر" },
{ "id": 8, "name": "تهران" },
{ "id": 9, "name": "چهارمحال وبختیاری" },
{ "id": 10, "name": "خراسان جنوبی" },
{ "id": 11, "name": "خراسان رضوی" },
{ "id": 12, "name": "خراسان شمالی" },
{ "id": 13, "name": "خوزستان" },
{ "id": 14, "name": "زنجان" },
{ "id": 15, "name": "سمنان" },
{ "id": 16, "name": "سیستان وبلوچستان" },
{ "id": 17, "name": "فارس" },
{ "id": 18, "name": "قزوین" },
{ "id": 19, "name": "قم" },
{ "id": 20, "name": "کردستان" },
{ "id": 21, "name": "کرمان" },
{ "id": 22, "name": "کرمانشاه" },
{ "id": 23, "name": "کهگیلویه وبویراحمد" },
{ "id": 24, "name": "گلستان" },
{ "id": 25, "name": "گیلان" },
{ "id": 26, "name": "لرستان" },
{ "id": 27, "name": "مازندران" },
{ "id": 28, "name": "مرکزی" },
{ "id": 29, "name": "هرمزگان" },
{ "id": 30, "name": "همدان" },
{ "id": 31, "name": "یزد" }
]
File diff suppressed because it is too large Load Diff
+671
View File
@@ -0,0 +1,671 @@
import { faker } from "@faker-js/faker";
import { CreateProductStep, ProductSource, ProductStatus } from "../../common/enums/product.enum";
import { SellerType } from "../../common/enums/seller.enum";
import { OwnerRef } from "../../modules/shop/models/Abstraction/IShop";
faker.locale = "fa";
export const fakeImageGenerator = () => faker.image.imageUrl();
export const CreateFakeSellerData = () => ({
fullName: faker.name.fullName(),
shebaNumber: faker.finance.iban(),
dateOfBirth: "1360/08/25",
phoneNumber: faker.phone.number("0912#######"),
telephoneNumber: faker.phone.number("0863#######"),
nationalCode: faker.random.numeric(10),
email: faker.internet.email(crypto.randomUUID().slice(0, 5)),
profilePhoto: faker.image.abstract(),
type: faker.helpers.arrayElement([SellerType.RETAILER, SellerType.WHOLESALER]),
});
export const createFakeShopData = (shipmentMethod: number[], owner: string) => ({
shopName: `فروشگاه ${faker.commerce.department()}`,
shopDescription: "تست",
telephoneNumber: faker.phone.number("0863#######"),
shopNationalId: faker.random.numeric(10),
shopPostalCode: faker.random.numeric(10),
logo: faker.image.imageUrl(),
rate: {
totalRate: faker.datatype.number({ min: 1, max: 5 }),
totalCount: faker.datatype.number({ min: 1, max: 1000 }),
onTimeShip: faker.datatype.number({ min: 1, max: 5 }),
returns: faker.datatype.number({ min: 1, max: 5 }),
cancel: faker.datatype.number({ min: 1, max: 5 }),
},
shipmentMethod,
owner,
ownerRef: OwnerRef.SELLER,
});
export const fakeProductsData = [
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "تلویزیون ال ای دی سامسونگ",
// title_en: "Samsung LED TV",
// model: "SAMSUNG123",
// source: ProductSource.LOCAL,
// description: "تلویزیون ال ای دی 55 اینچ سامسونگ با کیفیت تصویر 4K",
// metaDescription: "تلویزیون ال ای دی سامسونگ 4K",
// tags: ["ال ای دی", "سامسونگ", "4K"],
// advantages: ["کیفیت تصویر عالی", "قاب باریک"],
// disAdvantages: ["قیمت بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "ماشین لباسشویی ال جی",
// title_en: "LG Washing Machine",
// model: "LGWASH456",
// source: ProductSource.IMPORT,
// description: "ماشین لباسشویی 8 کیلوگرمی ال جی با تکنولوژی بخار شوی",
// metaDescription: "ماشین لباسشویی ال جی با ظرفیت 8 کیلوگرم",
// tags: ["لباسشویی", "ال جی", "بخار شوی"],
// advantages: ["مصرف انرژی کم", "سرعت چرخش بالا"],
// disAdvantages: ["صدا در حالت خشک کن"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "یخچال ساید بای ساید بوش",
// title_en: "Bosch Side by Side Refrigerator",
// model: "BOSCHFRIDGE789",
// source: ProductSource.LOCAL,
// description: "یخچال فریزر ساید بای ساید بوش با ظرفیت 30 فوت",
// metaDescription: "یخچال فریزر ساید بای ساید بوش",
// tags: ["یخچال", "بوش", "ساید بای ساید"],
// advantages: ["ظرفیت بالا", "طراحی مدرن"],
// disAdvantages: ["قیمت بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
{
color: true,
size: false,
meterage: false,
title_fa: "موبایل آیفون 13",
title_en: "iPhone 13",
model: "IPHONE13PRO",
source: ProductSource.IMPORT,
description: "گوشی آیفون 13 پرو با حافظه 256 گیگابایت و دوربین سه‌گانه",
metaDescription: "آیفون 13 پرو با حافظه 256 گیگ",
tags: ["آیفون", "اپل", "موبایل"],
advantages: ["دوربین عالی", "عملکرد سریع"],
disAdvantages: ["قیمت بسیار بالا"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: true,
size: false,
meterage: false,
title_fa: "لپ‌تاپ ایسوس",
title_en: "Asus Laptop",
model: "ASUSLAP234",
source: ProductSource.LOCAL,
description: "لپ‌تاپ ایسوس با پردازنده Core i7 و حافظه 16 گیگابایت",
metaDescription: "لپ‌تاپ ایسوس با حافظه 16 گیگ",
tags: ["لپ‌تاپ", "ایسوس", "Core i7"],
advantages: ["عملکرد قوی", "صفحه‌نمایش با کیفیت"],
disAdvantages: ["عمر باتری پایین"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: true,
size: false,
meterage: false,
title_fa: "ساعت هوشمند اپل واچ",
title_en: "Apple Watch",
model: "APPLEWATCH123",
source: ProductSource.IMPORT,
description: "ساعت هوشمند اپل با قابلیت اندازه‌گیری ضربان قلب",
metaDescription: "اپل واچ سری 6",
tags: ["ساعت هوشمند", "اپل", "واچ"],
advantages: ["اندازه‌گیری سلامت", "طراحی زیبا"],
disAdvantages: ["عمر باتری محدود"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "تبلت سامسونگ گلکسی تب",
// title_en: "Samsung Galaxy Tab",
// model: "SAMGALTAB567",
// source: ProductSource.LOCAL,
// description: "تبلت سامسونگ گلکسی تب با صفحه نمایش 10.5 اینچ",
// metaDescription: "تبلت سامسونگ گلکسی",
// tags: ["تبلت", "سامسونگ", "گلکسی"],
// advantages: ["صفحه نمایش بزرگ", "قابلیت چند وظیفگی"],
// disAdvantages: ["عمر باتری متوسط"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "دوربین عکاسی نیکون",
// title_en: "Nikon Camera",
// model: "NIKONCAM123",
// source: ProductSource.IMPORT,
// description: "دوربین دیجیتال نیکون با سنسور 24 مگاپیکسل",
// metaDescription: "دوربین نیکون دیجیتال",
// tags: ["دوربین", "نیکون", "دیجیتال"],
// advantages: ["کیفیت تصویر بالا", "سنسور قوی"],
// disAdvantages: ["بدنه سنگین"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "اسپیکر بلوتوثی جی بی ال",
// title_en: "JBL Bluetooth Speaker",
// model: "JBLSPK789",
// source: ProductSource.LOCAL,
// description: "اسپیکر بلوتوثی جی بی ال با کیفیت صدای عالی",
// metaDescription: "اسپیکر جی بی ال بلوتوث",
// tags: ["اسپیکر", "بلوتوث", "جی بی ال"],
// advantages: ["کیفیت صدای عالی", "قابلیت حمل آسان"],
// disAdvantages: ["قیمت نسبتاً بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "هدفون بی سیم سونی",
// title_en: "Sony Wireless Headphones",
// model: "SONYWH123",
// source: ProductSource.IMPORT,
// description: "هدفون بی سیم سونی با قابلیت حذف نویز",
// metaDescription: "هدفون بی سیم سونی",
// tags: ["هدفون", "بی سیم", "سونی"],
// advantages: ["حذف نویز عالی", "طراحی ارگونومیک"],
// disAdvantages: ["قیمت بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "دوربین فیلمبرداری سونی",
// title_en: "Sony Camcorder",
// model: "SONYCAM456",
// source: ProductSource.LOCAL,
// description: "دوربین فیلمبرداری سونی با قابلیت فیلم‌برداری 4K",
// metaDescription: "دوربین فیلمبرداری 4K سونی",
// tags: ["دوربین", "فیلمبرداری", "سونی"],
// advantages: ["فیلم‌برداری 4K", "باتری با عمر طولانی"],
// disAdvantages: ["وزن سنگین"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "گوشی شیائومی می 11",
// title_en: "Xiaomi Mi 11",
// model: "MI11PRO",
// source: ProductSource.IMPORT,
// description: "گوشی موبایل شیائومی می 11 پرو با حافظه 256 گیگابایت",
// metaDescription: "گوشی شیائومی می 11 پرو",
// tags: ["شیائومی", "موبایل", "می 11"],
// advantages: ["دوربین قوی", "پردازنده سریع"],
// disAdvantages: ["شارژر جداگانه فروخته می‌شود"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "تلویزیون ال جی OLED",
// title_en: "LG OLED TV",
// model: "LGOLED789",
// source: ProductSource.LOCAL,
// description: "تلویزیون 65 اینچ ال جی با کیفیت تصویر OLED",
// metaDescription: "تلویزیون OLED ال جی",
// tags: ["تلویزیون", "ال جی", "OLED"],
// advantages: ["کیفیت تصویر عالی", "طراحی باریک"],
// disAdvantages: ["قیمت بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "پاوربانک انکر",
// title_en: "Anker Power Bank",
// model: "ANKERPB100",
// source: ProductSource.IMPORT,
// description: "پاوربانک انکر با ظرفیت 20000 میلی‌آمپر ساعت",
// metaDescription: "پاوربانک انکر با ظرفیت بالا",
// tags: ["پاوربانک", "انکر", "ظرفیت بالا"],
// advantages: ["ظرفیت بزرگ", "شارژ سریع"],
// disAdvantages: ["وزن بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: false,
// size: false,
// meterage: false,
// title_fa: "کنسول بازی پلی استیشن 5",
// title_en: "PlayStation 5",
// model: "PS5DISC",
// source: ProductSource.IMPORT,
// description: "کنسول بازی پلی استیشن 5 با دیسک درایو",
// metaDescription: "کنسول بازی پلی استیشن 5",
// tags: ["کنسول", "پلی استیشن", "بازی"],
// advantages: ["بازی‌های انحصاری", "گرافیک فوق‌العاده"],
// disAdvantages: ["موجودی کم"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "هدفون بی سیم بوز",
// title_en: "Bose Wireless Headphones",
// model: "BOSEWH789",
// source: ProductSource.IMPORT,
// description: "هدفون بی سیم بوز با قابلیت حذف نویز فعال",
// metaDescription: "هدفون بی سیم بوز",
// tags: ["هدفون", "بی سیم", "بوز"],
// advantages: ["کیفیت صدای بالا", "حذف نویز"],
// disAdvantages: ["قیمت بالا"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "تبلت هواوی مدیاپد",
// title_en: "Huawei MediaPad",
// model: "HUAWEITAB1",
// source: ProductSource.LOCAL,
// description: "تبلت هواوی مدیاپد با صفحه نمایش 10.8 اینچی",
// metaDescription: "تبلت هواوی مدیاپد",
// tags: ["تبلت", "هواوی", "مدیاپد"],
// advantages: ["صفحه نمایش بزرگ", "طراحی سبک"],
// disAdvantages: ["عملکرد متوسط در بازی"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "اسپیکر بلوتوثی سونی",
// title_en: "Sony Bluetooth Speaker",
// model: "SONYSPK234",
// source: ProductSource.IMPORT,
// description: "اسپیکر بلوتوثی سونی با کیفیت صدای عالی",
// metaDescription: "اسپیکر بلوتوثی سونی",
// tags: ["اسپیکر", "بلوتوث", "سونی"],
// advantages: ["صدای شفاف", "طراحی مقاوم در برابر آب"],
// disAdvantages: ["عمر باتری محدود"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "موبایل نوکیا G20",
// title_en: "Nokia G20",
// model: "NOKIAG20",
// source: ProductSource.LOCAL,
// description: "گوشی موبایل نوکیا G20 با حافظه 128 گیگابایت",
// metaDescription: "گوشی نوکیا G20",
// tags: ["نوکیا", "موبایل", "G20"],
// advantages: ["باتری بادوام", "طراحی مقاوم"],
// disAdvantages: ["دوربین متوسط"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: true,
// size: false,
// meterage: false,
// title_fa: "مایکروویو پاناسونیک",
// title_en: "Panasonic Microwave",
// model: "PANAMW234",
// source: ProductSource.IMPORT,
// description: "مایکروویو پاناسونیک با قدرت 1200 وات",
// metaDescription: "مایکروویو پاناسونیک",
// tags: ["مایکروویو", "پاناسونیک", "پخت سریع"],
// advantages: ["پخت سریع", "قابلیت گریل"],
// disAdvantages: ["فضای داخلی کوچک"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
{
color: false,
size: true,
meterage: false,
title_fa: "تی شرت مردانه",
title_en: "mardane t-shirt",
model: "Nike",
source: ProductSource.IMPORT,
description: "تی شرت سبک و راحت مردانه نایک",
metaDescription: "تی شرت مردانه نایک برای ورزش و استفاده روزمره",
tags: ["تی شرت", "نایک", "لباس مردانه"],
advantages: ["پارچه سبک", "مناسب برای تابستان"],
disAdvantages: ["محدودیت رنگ"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: false,
size: false,
meterage: true,
title_fa: "فرش ماشینی",
title_en: "farsh mashini",
model: "Iranian Classic",
source: ProductSource.LOCAL,
description: "فرش ماشینی با طرح کلاسیک ایرانی",
metaDescription: "فرش ماشینی با کیفیت بالا و طرح سنتی ایرانی",
tags: ["فرش", "ماشینی", "کلاسیک"],
advantages: ["طرح زیبا", "قابلیت شستشو"],
disAdvantages: ["سنگین بودن"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: false,
size: true,
meterage: false,
title_fa: "شلوار جین زنانه",
title_en: "zanane jean pants",
model: "Levi's",
source: ProductSource.IMPORT,
description: "شلوار جین زنانه با استایل اسپرت",
metaDescription: "شلوار جین زنانه مناسب برای استفاده روزمره",
tags: ["شلوار جین", "زنانه", "لباس"],
advantages: ["کیفیت بالا", "راحتی"],
disAdvantages: ["قیمت بالا"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: false,
size: true,
meterage: false,
title_fa: "پیراهن مجلسی زنانه",
title_en: "zanane dress",
model: "Zara",
source: ProductSource.IMPORT,
description: "پیراهن مجلسی شیک زنانه برند زارا",
metaDescription: "پیراهن زنانه شیک برای مجالس و مهمانی‌ها",
tags: ["پیراهن", "مجلسی", "زنانه", "زارا"],
advantages: ["مدل زیبا", "جنس با کیفیت"],
disAdvantages: ["حساس به شستشو"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: false,
size: false,
meterage: true,
title_fa: "پرده اتاق خواب",
title_en: "bedroom curtain",
model: "LuxDecor",
source: ProductSource.LOCAL,
description: "پرده اتاق خواب با طرح مدرن و زیبا",
metaDescription: "پرده اتاق خواب شیک و جذاب با جنس مقاوم",
tags: ["پرده", "اتاق خواب", "دکوراسیون"],
advantages: ["نورگیر خوب", "نصب آسان"],
disAdvantages: ["قابلیت چین‌خوردگی"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
// {
// color: false,
// size: true,
// meterage: false,
// title_fa: "کفش ورزشی مردانه",
// title_en: "sport shoes",
// model: "Adidas",
// source: ProductSource.IMPORT,
// description: "کفش ورزشی مردانه مناسب دویدن",
// metaDescription: "کفش ورزشی سبک و راحت برای فعالیت‌های ورزشی",
// tags: ["کفش", "ورزشی", "مردانه", "آدیداس"],
// advantages: ["کفی نرم", "سبک"],
// disAdvantages: ["عدم مقاومت در برابر آب"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
{
color: false,
size: false,
meterage: true,
title_fa: "کفپوش چوبی",
title_en: "wooden flooring",
model: "WoodMaster",
source: ProductSource.LOCAL,
description: "کفپوش چوبی با کیفیت برای اتاق‌ها و سالن",
metaDescription: "کفپوش چوبی مقاوم با ظاهر زیبا",
tags: ["کفپوش", "چوبی", "دکوراسیون"],
advantages: ["زیبایی طبیعی", "مقاومت بالا"],
disAdvantages: ["نیاز به مراقبت ویژه"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
{
color: false,
size: true,
meterage: false,
title_fa: "کت چرمی مردانه",
title_en: "leather jacket",
model: "Tommy Hilfiger",
source: ProductSource.IMPORT,
description: "کت چرمی شیک مردانه برند تامی هیلفیگر",
metaDescription: "کت چرمی مردانه با کیفیت و استایل خاص",
tags: ["کت", "چرمی", "مردانه", "تامی"],
advantages: ["گرم و بادوام", "استایل خاص"],
disAdvantages: ["نیاز به مراقبت از چرم"],
isFake: false,
imagesUrl: {
cover: faker.image.imageUrl(),
list: [faker.image.imageUrl(), faker.image.imageUrl()],
},
step: CreateProductStep.Image,
status: ProductStatus.Approved,
},
// {
// color: false,
// size: true,
// meterage: false,
// title_fa: "هودی دخترانه",
// title_en: "hoodie",
// model: "Puma",
// source: ProductSource.IMPORT,
// description: "هودی گرم و راحت دخترانه برند پوما",
// metaDescription: "هودی دخترانه مناسب فصل پاییز و زمستان",
// tags: ["هودی", "دخترانه", "پوشاک", "پوما"],
// advantages: ["گرم و نرم", "کلاه قابل تنظیم"],
// disAdvantages: ["محدودیت رنگ"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
// {
// color: false,
// size: false,
// meterage: true,
// title_fa: "کاغذ دیواری مدرن",
// title_en: "modern wallpaper",
// model: "ArtWall",
// source: ProductSource.IMPORT,
// description: "کاغذ دیواری مدرن با طرح‌های جدید و زیبا",
// metaDescription: "کاغذ دیواری با طرح‌های مدرن برای دکوراسیون داخلی",
// tags: ["کاغذ دیواری", "دکوراسیون", "مدرن"],
// advantages: ["نصب آسان", "قابلیت شستشو"],
// disAdvantages: ["نیاز به دیوار صاف"],
// isFake: false,
// imagesUrl: {
// cover: faker.image.imageUrl(),
// list: [faker.image.imageUrl(), faker.image.imageUrl()],
// },
// step: CreateProductStep.Image,
// status: ProductStatus.Approved,
// },
];
+45
View File
@@ -0,0 +1,45 @@
import mongoose from "mongoose";
import { CategoryModel } from "../../modules/category/models/category.model";
// Create the Category model
export async function backfillHierarchy() {
try {
console.log("Starting hierarchy backfill...");
// Fetch all categories
const categories = await CategoryModel.find();
// Helper function to recursively update hierarchy
const updateHierarchy = async (categoryId: mongoose.Types.ObjectId, parentHierarchy: mongoose.Types.ObjectId[] = []) => {
const category = await CategoryModel.findById(categoryId);
if (!category) return;
const currentHierarchy = [...parentHierarchy, category._id];
// Update the category's hierarchy
category.hierarchy = currentHierarchy;
await category.save();
// Find child categories and update their hierarchy
const childCategories = await CategoryModel.find({ parent: category._id });
for (const child of childCategories) {
await updateHierarchy(child._id, currentHierarchy);
}
};
// Find all top-level categories (categories with no parent)
const topLevelCategories = categories.filter((category) => !category.parent);
// Update hierarchy for all top-level categories and their descendants
for (const topCategory of topLevelCategories) {
await updateHierarchy(topCategory._id);
}
console.log("Hierarchy backfill completed successfully!");
} catch (err) {
console.error("Error during hierarchy backfill:", err);
}
}
+41
View File
@@ -0,0 +1,41 @@
import { Logger } from "../../core/logging/logger";
import { BlogCategoryModel } from "../../modules/blog/models/blogCategory.model";
export const blogCategorySeeder = async (logger: Logger) => {
try {
const categories = [
{
title_fa: "الکترونیک",
slug: "electronics",
description: "آخرین اخبار و مقالات مربوط به محصولات الکترونیکی",
},
{
title_fa: "مد و پوشاک",
slug: "fashion",
description: "مقالات درباره مد، پوشاک و جدیدترین ترندهای فشن",
},
{
title_fa: "خانه و آشپزخانه",
slug: "home-kitchen",
description: "راهنمایی‌ها و مقالات مرتبط با محصولات خانه و آشپزخانه",
},
{
title_fa: "زیبایی و سلامت",
slug: "beauty-health",
description: "مقالات درباره زیبایی، مراقبت از پوست و سلامت",
},
{
title_fa: "بازی و سرگرمی",
slug: "gaming-entertainment",
description: "آخرین اخبار و مقالات مربوط به بازی‌ها و سرگرمی‌ها",
},
];
logger.info("start seeding Blog categories.");
await BlogCategoryModel.insertMany(categories);
logger.info("Blog categories seeded successfully");
} catch (error) {
logger.error("Error seeding blog categories:", error);
}
};
+239
View File
@@ -0,0 +1,239 @@
import { Logger } from "../../core/logging/logger";
import { AdminModel } from "../../modules/admin/models/admin.model";
import { IBlog } from "../../modules/blog/models/Abstraction/IBlog";
import { BlogModel } from "../../modules/blog/models/blog.model";
import { BlogCategoryModel } from "../../modules/blog/models/blogCategory.model";
import { fakeImageGenerator } from "../data/product";
export const blogSeeder = async (logger: Logger) => {
try {
const admin = await AdminModel.findOne({ userName: "defaultAdmin" });
const categories = await BlogCategoryModel.find();
if (categories.length === 0) {
logger.error("No blog categories found. Please run the blogCategorySeeder first.");
throw new Error("No blog categories found. Please run the blogCategorySeeder first.");
}
const blogs: IBlog[] = [];
categories.forEach((category) => {
switch (category.slug) {
case "electronics":
blogs.push(
{
title_fa: "برترین گوشی‌های هوشمند سال ۲۰۲۴",
slug: "top-smartphones-2024",
category: category._id,
description: "مروری بر بهترین گوشی‌های هوشمند عرضه شده در سال ۲۰۲۴.",
content:
"در سال ۲۰۲۴، شرکت‌های بزرگ تکنولوژی گوشی‌های هوشمند جدیدی معرفی کردند که شامل ویژگی‌ها و قابلیت‌های پیشرفته‌ای هستند...",
author: admin!._id,
tags: ["گوشی", "هوشمند", "تکنولوژی"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "راهنمای خرید لپ‌تاپ مناسب برای دانشجویان",
slug: "laptop-buying-guide-for-students",
category: category._id,
description: "نکاتی که دانشجویان باید هنگام خرید لپ‌تاپ مد نظر داشته باشند.",
content:
"برای دانشجویان، انتخاب لپ‌تاپ مناسب اهمیت زیادی دارد. عواملی مانند وزن، عمر باتری، قدرت پردازش و قیمت باید در نظر گرفته شوند...",
author: admin!._id,
tags: ["لپ‌تاپ", "خرید", "راهنما"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "تکنولوژی‌های نوین در تلویزیون‌های هوشمند",
slug: "innovative-technologies-in-smart-tvs",
category: category._id,
description: "بررسی آخرین تکنولوژی‌های موجود در تلویزیون‌های هوشمند.",
content:
"تلویزیون‌های هوشمند امروزه با تکنولوژی‌های پیشرفته‌ای مانند 4K، OLED، HDR و سیستم‌های هوش مصنوعی مجهز شده‌اند که تجربه تماشای شما را بهبود می‌بخشند...",
author: admin!._id,
tags: ["تلویزیون", "هوشمند", "تکنولوژی"],
imageUrl: fakeImageGenerator(),
view: 0,
},
);
break;
case "fashion":
blogs.push(
{
title_fa: "جدیدترین ترندهای مد بهار ۲۰۲۴",
slug: "latest-spring-fashion-trends-2024",
category: category._id,
description: "مروری بر جدیدترین ترندهای مد فصل بهار ۲۰۲۴.",
content:
"فصل بهار ۲۰۲۴ با ورود رنگ‌های شاد، الگوهای گلدار و پارچه‌های سبک همراه است. این ساله تاکید بیشتری بر روی راحتی و استایل کلاسیک دیده می‌شود...",
author: admin!._id,
tags: ["مد", "بهار", "ترند"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "چگونه لباس مناسب را برای یک مراسم رسمی انتخاب کنیم",
slug: "choosing-suitable-clothes-for-formal-events",
category: category._id,
description: "نکاتی برای انتخاب لباس مناسب جهت شرکت در مراسم‌های رسمی.",
content:
"انتخاب لباس مناسب برای مراسم‌های رسمی می‌تواند تاثیر زیادی بر روی حضور شما داشته باشد. در این مقاله به بررسی نکاتی که باید هنگام انتخاب لباس رسمی در نظر بگیرید می‌پردازیم...",
author: admin!._id,
tags: ["لباس", "رسمی", "راهنما"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "استایل‌های مردانه محبوب در سال ۲۰۲۴",
slug: "popular-mens-fashion-styles-2024",
category: category._id,
description: "بررسی استایل‌های محبوب و جدید در مد مردانه سال ۲۰۲۴.",
content:
"مد مردانه در سال ۲۰۲۴ با ترکیب کلاسیک و مدرن، سبک‌های متنوعی را ارائه می‌دهد. از کت و شلوارهای شیک گرفته تا استایل‌های کژوال و خیابانی، انتخاب‌های زیادی وجود دارد...",
author: admin!._id,
tags: ["مد", "مردانه", "استایل"],
imageUrl: fakeImageGenerator(),
view: 0,
},
);
break;
case "home-kitchen":
blogs.push(
{
title_fa: "بهترین لوازم آشپزخانه برای هر خانه",
slug: "best-kitchen-appliances-for-every-home",
category: category._id,
description: "نکاتی برای انتخاب بهترین لوازم آشپزخانه مناسب برای خانه‌های مختلف.",
content:
"لوازم آشپزخانه نقش مهمی در راحتی و کارایی خانه دارند. از ماشین لباسشویی گرفته تا فرهای هوشمند، انتخاب صحیح می‌تواند تجربه آشپزی شما را بهبود بخشد...",
author: admin!._id,
tags: ["آشپزخانه", "لوازم", "راهنما"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "نکات طراحی داخلی خانه برای فضاهای کوچک",
slug: "interior-design-tips-for-small-spaces",
category: category._id,
description: "راهنمایی‌هایی برای طراحی داخلی مناسب فضاهای کوچک خانه.",
content:
"طراحی داخلی فضاهای کوچک نیازمند استفاده بهینه از فضا و انتخاب مبلمان چندکاره است. در این مقاله به بررسی نکاتی که می‌تواند به شما در طراحی منزل کوچک کمک کند می‌پردازیم...",
author: admin!._id,
tags: ["طراحی داخلی", "خانه کوچک", "نکات"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "روش‌های سازماندهی و بهینه‌سازی فضای آشپزخانه",
slug: "organizing-and-optimizing-kitchen-space",
category: category._id,
description: "نکاتی برای سازماندهی و بهینه‌سازی فضای آشپزخانه.",
content:
"یک آشپزخانه سازماندهی شده نه تنها زیبایی دارد بلکه کارایی بیشتری نیز دارد. از استفاده از کشوهای چندمنظوره تا نگهداری مناسب ابزارهای آشپزی، این مقاله به شما کمک می‌کند تا آشپزخانه‌تان را بهینه کنید...",
author: admin!._id,
tags: ["آشپزخانه", "سازماندهی", "بهینه‌سازی"],
imageUrl: fakeImageGenerator(),
view: 0,
},
);
break;
case "beauty-health":
blogs.push(
{
title_fa: "رازهای داشتن پوستی سالم و درخشان",
slug: "secrets-to-healthy-glowing-skin",
category: category._id,
description: "نکاتی برای داشتن پوستی سالم، درخشان و زیبا.",
content:
"پوستی سالم نیازمند مراقبت‌های منظم و استفاده از محصولات مناسب است. در این مقاله به بررسی رازهایی که می‌تواند به شما کمک کند پوستی درخشان و زیبا داشته باشید می‌پردازیم...",
author: admin!._id,
tags: ["زیبایی", "سلامت پوست", "راهنما"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "فواید ورزش منظم برای سلامت روانی",
slug: "benefits-of-regular-exercise-for-mental-health",
category: category._id,
description: "بررسی فواید ورزش منظم بر سلامت روانی انسان.",
content:
"ورزش نه تنها به بهبود سلامت جسمی کمک می‌کند بلکه تاثیرات مثبت زیادی بر سلامت روانی دارد. از کاهش استرس تا افزایش اعتماد به نفس، در این مقاله به بررسی این فواید می‌پردازیم...",
author: admin!._id,
tags: ["ورزش", "سلامت روان", "فواید"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "مراقبت از موهای خشک و آسیب‌دیده",
slug: "caring-for-dry-damaged-hair",
category: category._id,
description: "نکاتی برای مراقبت و ترمیم موهای خشک و آسیب‌دیده.",
content:
"موهای خشک و آسیب‌دیده نیازمند مراقبت ویژه‌ای هستند. از استفاده از شامپوهای مرطوب‌کننده تا ماسک‌های تغذیه‌کننده، در این مقاله به بررسی روش‌هایی که می‌تواند به ترمیم و تقویت موهای شما کمک کند می‌پردازیم...",
author: admin!._id,
tags: ["مو", "زیبایی", "مراقبت"],
imageUrl: fakeImageGenerator(),
view: 0,
},
);
break;
case "gaming-entertainment":
blogs.push(
{
title_fa: "پربازترین بازی‌های ویدیویی سال ۲۰۲۴",
slug: "most-played-video-games-2024",
category: category._id,
description: "مروری بر پربازترین و محبوب‌ترین بازی‌های ویدیویی عرضه شده در سال ۲۰۲۴.",
content:
"سال ۲۰۲۴ با عرضه بازی‌های ویدیویی هیجان‌انگیزی همراه بوده است. از بازی‌های اکشن گرفته تا RPGها، این مقاله به معرفی پربازترین و محبوب‌ترین بازی‌های سال می‌پردازد...",
author: admin!._id,
tags: ["بازی", "ویدیوئی", "مرور"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "نکاتی برای بهبود مهارت‌های بازی‌های رقابتی",
slug: "tips-to-improve-competitive-gaming-skills",
category: category._id,
description: "راهنمایی‌هایی برای ارتقاء مهارت‌ها و موفقیت در بازی‌های رقابتی.",
content:
"بازی‌های رقابتی نیازمند تمرین مداوم و استراتژی‌های هوشمندانه هستند. در این مقاله به بررسی نکاتی می‌پردازیم که می‌تواند به شما کمک کند مهارت‌های بازی رقابتی خود را بهبود ببخشید...",
author: admin!._id,
tags: ["بازی", "رقابتی", "مهارت"],
imageUrl: fakeImageGenerator(),
view: 0,
},
{
title_fa: "بررسی جدیدترین کنسول‌های بازی",
slug: "review-of-latest-gaming-consoles",
category: category._id,
description: "مروری بر جدیدترین کنسول‌های بازی و ویژگی‌های آن‌ها.",
content:
"کنسول‌های بازی جدید با تکنولوژی‌های پیشرفته‌ای عرضه شده‌اند که تجربه بازی را به سطح بالاتری می‌برند. در این مقاله به بررسی ویژگی‌ها و عملکرد جدیدترین کنسول‌های بازی می‌پردازیم...",
author: admin!._id,
tags: ["کنسول", "بازی", "مرور"],
imageUrl: fakeImageGenerator(),
view: 0,
},
);
break;
default:
console.warn(`No seeder defined for category slug: ${category.slug}`);
break;
}
});
// Insert Blogs
await BlogModel.insertMany(blogs);
logger.info("Blogs seeded successfully");
} catch (error) {
logger.error("Error seeding blogs:", error);
}
};
+138
View File
@@ -0,0 +1,138 @@
import { Types } from "mongoose";
import { StatusEnum } from "../../common/enums/status.enum";
import { Logger } from "../../core/logging/logger";
import { BrandModel } from "../../modules/brand/models/brand.model";
import { CategoryModel } from "../../modules/category/models/category.model";
interface IBrandSeed {
title_fa: string;
title_en: string;
// category: Types.ObjectId;
}
const brandSeedMap: { [key: string]: IBrandSeed[] } = {
"گوشی موبایل": [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "اپل", title_en: "Apple" },
{ title_fa: "شیائومی", title_en: "Xiaomi" },
{ title_fa: "هواوی", title_en: "Huawei" },
{ title_fa: "نوکیا", title_en: "Nokia" },
],
تبلت: [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "اپل", title_en: "Apple" },
{ title_fa: "مایکروسافت", title_en: "Microsoft" },
{ title_fa: "لنوو", title_en: "Lenovo" },
{ title_fa: "هواوی", title_en: "Huawei" },
],
"خودکار و خودنویس": [
{ title_fa: "پایلوت", title_en: "Pilot" },
{ title_fa: "شیفر", title_en: "Sheaffer" },
{ title_fa: "پارکر", title_en: "Parker" },
{ title_fa: "لامی", title_en: "Lamy" },
],
لامپ: [
{ title_fa: "فیلیپس", title_en: "Philips" },
{ title_fa: "شیائومی", title_en: "Xiaomi" },
{ title_fa: "اپتونیک", title_en: "Optonique" },
{ title_fa: "نورکالا", title_en: "NoorKala" },
],
اسپیکر: [
{ title_fa: "جی بی ال", title_en: "JBL" },
{ title_fa: "سونی", title_en: "Sony" },
],
تلویزیون: [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "ال جی", title_en: "LG" },
{ title_fa: "سونی", title_en: "Sony" },
{ title_fa: "پاناسونیک", title_en: "Panasonic" },
],
"دوربین عکاسی": [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "ال جی", title_en: "LG" },
{ title_fa: "سونی", title_en: "Sony" },
{ title_fa: "پاناسونیک", title_en: "Nikon" },
],
"ساعت هوشمند": [
{ title_fa: "شیائومی", title_en: "Xiaomi" },
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "اپل", title_en: "Apple" },
{ title_fa: "هواوی", title_en: "Huawei" },
],
هدفون: [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "ال جی", title_en: "LG" },
{ title_fa: "سونی", title_en: "Sony" },
{ title_fa: "بوز", title_en: "Bose" },
],
"پاور بانک": [
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "آنکر", title_en: "Anker" },
],
"لپ تاپ": [
{ title_fa: "ایسر", title_en: "Acer" },
{ title_fa: "ایسوس", title_en: "Asus" },
{ title_fa: "اپل", title_en: "Apple" },
{ title_fa: "لنوو", title_en: "Lenovo" },
],
"لباس زنانه": [
{ title_fa: "زارا", title_en: "Zara" },
{ title_fa: "H&M", title_en: "H&M" },
{ title_fa: "گوچی", title_en: "Gucci" },
{ title_fa: "دیور", title_en: "Dior" },
],
"لوازم آشپزخانه": [
{ title_fa: "بوش", title_en: "Bosch" },
{ title_fa: "فیلیپس", title_en: "Philips" },
{ title_fa: "سامسونگ", title_en: "Samsung" },
{ title_fa: "پاناسونیک", title_en: "Panasonic" },
{ title_fa: "ال جی", title_en: "LG" },
],
"پلی استیشن": [{ title_fa: "سونی", title_en: "Sony" }],
متفرقه: [{ title_fa: "متفرقه", title_en: "malicious" }],
};
export const seedBrands = async (logger: Logger) => {
try {
logger.info("Brands seeding started");
const categoryMap: { [key: string]: Types.ObjectId } = {};
// Fetch all categories once
const categories = await CategoryModel.find();
categories.forEach((category) => {
categoryMap[category.title_fa] = category._id;
});
const brandDocuments = [];
// Prepare brand documents in bulk
for (const [categoryTitle, brands] of Object.entries(brandSeedMap)) {
const categoryId = categoryMap[categoryTitle];
if (categoryId) {
for (const brand of brands) {
const { title_fa, title_en } = brand;
brandDocuments.push({
title_fa,
title_en,
category: categoryId,
status: StatusEnum.Approved,
});
}
} else {
logger.warn(`Category '${categoryTitle}' not found`);
}
}
// Bulk insert
if (brandDocuments.length > 0) {
await BrandModel.insertMany(brandDocuments);
}
logger.info("Brands seeded successfully");
} catch (error) {
logger.error("Error seeding brands:", error);
}
};
+98
View File
@@ -0,0 +1,98 @@
import { Logger } from "../../core/logging/logger";
import { ColorModel } from "../../modules/category/models/color.model";
import { MeterageModel } from "../../modules/category/models/meterage.model";
import { SizeModel } from "../../modules/category/models/size.model";
const colors = [
{ name: "قرمز", hexColor: "#FF0000" },
{ name: "سبز", hexColor: "#00FF00" },
{ name: "آبی", hexColor: "#0000FF" },
{ name: "مشکی", hexColor: "#000000" },
{ name: "سفید", hexColor: "#FFFFFF" },
{ name: "زرد", hexColor: "#FFFF00" },
{ name: "بنفش", hexColor: "#800080" },
{ name: "نارنجی", hexColor: "#FFA500" },
{ name: "صورتی", hexColor: "#FFC0CB" },
{ name: "خاکستری", hexColor: "#808080" },
];
const sizes = [
{ value: "XS" },
{ value: "S" },
{ value: "M" },
{ value: "L" },
{ value: "XL" },
{ value: "XXL" },
{ value: "3XL" },
{ value: "4XL" },
{ value: "5XL" },
{ value: "6XL" },
];
const meterages = [
{ value: "1" },
{ value: "2" },
{ value: "3" },
{ value: "4" },
{ value: "5" },
{ value: "10" },
{ value: "15" },
{ value: "20" },
{ value: "25" },
{ value: "30" },
];
const seedColors = async () => {
try {
console.log("Seeding colors...");
await ColorModel.deleteMany({});
for (const color of colors) {
await ColorModel.create(color);
}
console.log("Colors seeded successfully");
} catch (error) {
console.error("Error seeding colors:", error);
}
};
const seedSizes = async () => {
try {
console.log("Seeding sizes...");
await SizeModel.deleteMany({});
for (const size of sizes) {
await SizeModel.create(size);
}
console.log("Sizes seeded successfully");
} catch (error) {
console.error("Error seeding sizes:", error);
}
};
const seedMeterages = async () => {
try {
console.log("Seeding meterages...");
await MeterageModel.deleteMany({});
for (const meterage of meterages) {
await MeterageModel.create(meterage);
}
console.log("Meterages seeded successfully");
} catch (error) {
console.error("Error seeding meterages:", error);
}
};
// Seed function
export const seedCatVariant = async (logger: Logger) => {
try {
logger.info("Categories variant seeding started");
await seedColors();
await seedSizes();
await seedMeterages();
console.log("Seeding complete!");
} catch (error) {
logger.error("Seeding error:", error);
}
};
+556
View File
@@ -0,0 +1,556 @@
import { faker } from "@faker-js/faker";
import { Types } from "mongoose";
faker.locale = "fa";
import { CategoryThemeEnum } from "../../common/enums/category.enum";
import { Logger } from "../../core/logging/logger";
import { AttributeValueModel } from "../../modules/category/models/attributeValue.model";
import { CategoryModel } from "../../modules/category/models/category.model";
import { CategoryAttributeModel } from "../../modules/category/models/CategoryAttribute.model";
import { ColorModel } from "../../modules/category/models/color.model";
import { MeterageModel } from "../../modules/category/models/meterage.model";
import { SizeModel } from "../../modules/category/models/size.model";
interface ICategorySeed {
title_fa: string;
title_en: string;
icon: string;
imageUrl: string;
description: string;
leaf: boolean;
theme?: CategoryThemeEnum;
parent?: Types.ObjectId;
}
interface ICategoryAttributeSeed {
title: string;
hint?: string;
type: string;
multiple: boolean;
required: boolean;
}
const rootCategories: ICategorySeed[] = [
{
title_fa: "موبایل و تبلت",
icon: "mobile",
imageUrl: faker.image.imageUrl(),
description: "لوازم جانبی",
title_en: "mobile",
leaf: false,
},
{
title_fa: "لوازم تحریر و اداری",
icon: "stationery",
description: "انواع لوازم تحریر و اداری",
title_en: "stationery-office",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "برق و الکتریکی",
icon: "electric",
description: "لوازم برق و الکتریکی",
title_en: "electric-electrical",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "لوازم و ابزارآلات",
icon: "tools",
description: "انواع لوازم و ابزارآلات",
title_en: "tools-equipment",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "صوت و تصویر",
icon: "audio-visual",
description: "انواع لوازم صوتی و تصویری",
title_en: "audio-visual",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "کامپیوتر",
icon: "computer",
description: "انواع کامپیوتر و لوازم جانبی",
title_en: "computer",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "کنسول بازی",
icon: "console",
description: "انواع کنسول بازی و لوازم جانبی",
title_en: "game-console",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "ورزش و سفر",
icon: "sports",
description: "لوازم ورزشی و سفر",
title_en: "sports-travel",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{ title_fa: "پوشاک و مد", icon: "fashion", description: "لباس و مد", title_en: "fashion", leaf: false, imageUrl: faker.image.imageUrl() },
{
title_fa: "لوازم خانگی",
icon: "home-appliances",
description: "لوازم خانه",
title_en: "home-appliances",
leaf: false,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "متفرقه",
icon: "malicious",
description: "متفرقه",
title_en: "malicious",
leaf: true,
imageUrl: faker.image.imageUrl(),
},
];
const subcategories: { [key: string]: ICategorySeed[] } = {
"موبایل و تبلت": [
{
title_fa: "گوشی موبایل",
icon: "phone",
description: "انواع گوشی های موبایل",
title_en: "mobile-phones",
leaf: true,
imageUrl: faker.image.imageUrl(),
theme: CategoryThemeEnum.Colored,
},
{
title_fa: "تبلت",
icon: "tablet",
description: "انواع تبلت ها",
title_en: "tablets",
leaf: true,
theme: CategoryThemeEnum.Colored,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "ساعت هوشمند",
icon: "watch",
description: "انواع ساعت هوشمند",
title_en: "smart-watch",
leaf: true,
theme: CategoryThemeEnum.Colored,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "پاور بانک",
icon: "watch",
description: "انواع پاور بانک",
title_en: "power-banks",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"لوازم تحریر و اداری": [
{
title_fa: "خودکار و خودنویس",
icon: "pen",
description: "انواع خودکار و خودنویس",
title_en: "pens",
leaf: true,
theme: CategoryThemeEnum.Colored,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "دفتر و کاغذ",
icon: "paper",
description: "انواع دفتر و کاغذ",
title_en: "paper-notebooks",
leaf: true,
theme: CategoryThemeEnum.Colored,
imageUrl: faker.image.imageUrl(),
},
],
"برق و الکتریکی": [
{
title_fa: "لامپ",
icon: "bulb",
description: "انواع لامپ ها",
title_en: "bulbs",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "کابل و سیم",
icon: "cable",
description: "انواع کابل و سیم",
title_en: "cables-wires",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"لوازم و ابزارآلات": [
{
title_fa: "آچار و پیچ گوشتی",
icon: "wrench",
description: "انواع آچار و پیچ گوشتی",
title_en: "wrenches-screwdrivers",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "دریل",
icon: "drill",
description: "انواع دریل ها",
title_en: "drills",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"صوت و تصویر": [
{
title_fa: "تلویزیون",
icon: "tv",
description: "انواع تلویزیون",
title_en: "televisions",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "اسپیکر",
icon: "speaker",
description: "انواع اسپیکر",
title_en: "speakers",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "هدفون",
icon: "headphone",
description: "انواع هدفون",
title_en: "headphones",
leaf: true,
theme: CategoryThemeEnum.Colored,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "دوربین عکاسی",
icon: "camera",
description: "انواع دوربین عکاسی",
title_en: "cameras",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
کامپیوتر: [
{
title_fa: "لپ تاپ",
icon: "laptop",
description: "انواع لپ تاپ",
title_en: "laptops",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "موس و کیبورد",
icon: "mouse-keyboard",
description: "انواع موس و کیبورد",
title_en: "mouse-keyboards",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"کنسول بازی": [
{
title_fa: "پلی استیشن",
icon: "playstation",
description: "انواع پلی استیشن",
title_en: "playstations",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "ایکس باکس",
icon: "xbox",
description: "انواع ایکس باکس",
title_en: "xbox",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"ورزش و سفر": [
{
title_fa: "تجهیزات ورزشی",
icon: "sports-equipment",
description: "انواع تجهیزات ورزشی",
title_en: "sports-equipment",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "لوازم سفر",
icon: "travel",
description: "انواع لوازم سفر",
title_en: "travel-equipment",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
"پوشاک و مد": [
{
title_fa: "لباس زنانه",
icon: "women-clothing",
description: "انواع لباس زنانه",
title_en: "women-clothing",
leaf: true,
theme: CategoryThemeEnum.Sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "لباس مردانه",
icon: "men-clothing",
description: "انواع لباس مردانه",
title_en: "men-clothing",
leaf: true,
theme: CategoryThemeEnum.Sized,
imageUrl: faker.image.imageUrl(),
},
],
"لوازم خانگی": [
{
title_fa: "لوازم آشپزخانه",
icon: "kitchen-appliances",
description: "انواع لوازم آشپزخانه",
title_en: "kitchen-appliances",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
{
title_fa: "لوازم نظافت",
icon: "cleaning-appliances",
description: "انواع لوازم نظافت",
title_en: "cleaning-appliances",
leaf: true,
theme: CategoryThemeEnum.No_color_No_sized,
imageUrl: faker.image.imageUrl(),
},
],
};
const categoryAttributesMap: { [key: string]: ICategoryAttributeSeed[] } = {
"گوشی موبایل": [
{ title: "باتری", hint: "ظرفیت باتری", type: "select", multiple: false, required: true },
{ title: "دوربین", hint: "کیفیت دوربین", type: "select", multiple: false, required: true },
{ title: "سیستم عامل", hint: "نوع سیستم عامل", type: "select", multiple: false, required: true },
{ title: "حافظه داخلی موبایل", hint: "ظرفیت حافظه", type: "select", multiple: false, required: true },
{ title: "پشتیبانی از 5G", hint: "آیا پشتیبانی می‌کند؟", type: "checkbox", multiple: false, required: false },
],
تبلت: [
{ title: "اندازه صفحه تبلت", hint: "اینچ", type: "select", multiple: false, required: true },
{ title: "پردازنده تبلت", hint: "نوع پردازنده", type: "select", multiple: false, required: true },
{ title: "دوربین جلو", hint: "کیفیت دوربین جلو", type: "select", multiple: false, required: false },
{ title: "ظرفیت باتری", hint: "میلی‌آمپر ساعت", type: "select", multiple: false, required: true },
{ title: "اتصال به شبکه", hint: "نوع شبکه", type: "select", multiple: false, required: false },
],
"خودکار و خودنویس": [
{ title: "نوع جوهر", hint: "جوهر مورد استفاده", type: "select", multiple: false, required: true },
{ title: "ضخامت نوشتاری", hint: "ضخامت نوک", type: "select", multiple: false, required: true },
{ title: "قابلیت شارژ مجدد", hint: "آیا قابل شارژ است؟", type: "checkbox", multiple: false, required: false },
{ title: "جنس بدنه خودکار", hint: "جنس بدنه خودکار", type: "select", multiple: false, required: true },
{ title: "رنگ جوهر", hint: "رنگ جوهر خودکار", type: "select", multiple: false, required: true },
],
لامپ: [
{ title: "نوع لامپ", hint: "نوع لامپ (LED، هالوژن و غیره)", type: "select", multiple: false, required: true },
{ title: "توان مصرفی لامپ", hint: "وات", type: "select", multiple: false, required: true },
{ title: "رنگ نور", hint: "رنگ نور", type: "select", multiple: false, required: true },
{ title: "طول عمر", hint: "ساعت", type: "select", multiple: false, required: true },
{ title: "قابلیت تنظیم شدت نور", hint: "آیا قابل تنظیم است؟", type: "checkbox", multiple: false, required: false },
],
تلویزیون: [
{ title: "اندازه صفحه تلویزیون", hint: "اینچ", type: "select", multiple: false, required: true },
{ title: "رزولوشن", hint: "کیفیت تصویر", type: "select", multiple: false, required: true },
{ title: "نوع صفحه", hint: "LED, OLED, QLED", type: "select", multiple: false, required: true },
{ title: "هوشمند", hint: "آیا هوشمند است؟", type: "checkbox", multiple: false, required: false },
{ title: "درگاه‌های ارتباطی", hint: "نوع و تعداد درگاه‌ها", type: "select", multiple: true, required: true },
],
"لپ تاپ": [
{ title: "پردازنده لپ تاپ", hint: "نوع پردازنده", type: "select", multiple: false, required: true },
{ title: "حافظه رم", hint: "ظرفیت حافظه رم", type: "select", multiple: false, required: true },
{ title: "حافظه داخلی", hint: "ظرفیت حافظه", type: "select", multiple: false, required: true },
{ title: "اندازه صفحه", hint: "اینچ", type: "select", multiple: false, required: true },
{ title: "کارت گرافیک", hint: "نوع کارت گرافیک", type: "select", multiple: false, required: true },
],
"لباس زنانه": [
{ title: "جنس پارچه", hint: "نوع پارچه", type: "select", multiple: false, required: true },
{ title: "نوع یقه", hint: "یقه لباس", type: "select", multiple: false, required: true },
{ title: "نوع آستین", hint: "آستین لباس", type: "select", multiple: false, required: true },
{ title: "مدل لباس", hint: "نوع و مدل لباس", type: "select", multiple: false, required: true },
{ title: "نوع شستشو", hint: "نحوه شستشو", type: "select", multiple: false, required: false },
],
"لوازم آشپزخانه": [
{ title: "توان مصرفی", hint: "توان لوازم برقی", type: "select", multiple: false, required: true },
{ title: "جنس بدنه", hint: "جنس بدنه دستگاه", type: "select", multiple: false, required: true },
{ title: "ظرفیت", hint: "ظرفیت دستگاه", type: "select", multiple: false, required: true },
{ title: "قابلیت تنظیم", hint: "آیا قابلیت تنظیم دارد؟", type: "checkbox", multiple: false, required: false },
{ title: "نوع کاربری", hint: "کاربری دستگاه", type: "select", multiple: false, required: true },
],
// Add more categories as needed
};
const attributeValuesMap: { [key: string]: string[] } = {
باتری: ["3000mAh", "4000mAh", "5000mAh", "6000mAh", "7000mAh"],
دوربین: ["12MP", "16MP", "20MP", "48MP", "108MP"],
"سیستم عامل": ["اندروید", "iOS", "ویندوز", "لینوکس", "سایر"],
"حافظه داخلی موبایل": ["32GB", "64GB", "128GB", "256GB", "512GB"],
"پشتیبانی از 5G": ["بله", "خیر"],
"اندازه صفحه تبلت": ["7 اینچ", "10 اینچ", "13 اینچ", "15 اینچ", "17 اینچ"],
"پردازنده تبلت": ["Intel", "AMD", "Apple M1", "Qualcomm", "MediaTek"],
"دوربین جلو": ["5MP", "8MP", "12MP", "16MP", "20MP"],
"ظرفیت باتری": ["2000mAh", "3000mAh", "4000mAh", "5000mAh", "6000mAh"],
"اتصال به شبکه": ["WiFi", "4G", "5G", "Ethernet", "بلوتوث"],
"نوع جوهر": ["ژل", "روغنی", "آبی", "خودکار", "پلاستیکی"],
"ضخامت نوشتاری": ["0.5mm", "0.7mm", "1.0mm", "1.2mm", "1.5mm"],
"قابلیت شارژ مجدد": ["بله", "خیر"],
"جنس بدنه خودکار": ["فلز", "پلاستیک", "چوب", "سرامیک", "کربن"],
"رنگ جوهر": ["مشکی", "آبی", "قرمز", "سبز", "بنفش"],
"نوع لامپ": ["LED", "هالوژن", "فلورسنت", "رشته‌ای", "پلاسما"],
"توان مصرفی لامپ": ["5W", "10W", "15W", "20W", "25W"],
"رنگ نور": ["سفید", "زرد", "آبی", "سبز", "قرمز"],
"طول عمر": ["1000 ساعت", "5000 ساعت", "10000 ساعت", "20000 ساعت", "50000 ساعت"],
"قابلیت تنظیم شدت نور": ["بله", "خیر"],
"اندازه صفحه تلویزیون": ["20 اینچ", "32 اینچ", "40 اینچ", "60 اینچ", "70 اینچ"],
رزولوشن: ["720p", "1080p", "4K", "8K", "HDR"],
"نوع صفحه": ["LED", "OLED", "QLED", "Plasma", "LCD"],
هوشمند: ["بله", "خیر"],
"درگاه‌های ارتباطی": ["HDMI", "USB", "Bluetooth", "WiFi", "Ethernet"],
"پردازنده لپ تاپ": ["Intel", "AMD", "Apple M1", "Qualcomm", "Apple M2"],
"حافظه رم": ["4GB", "8GB", "16GB", "32GB", "64GB"],
"حافظه داخلی": ["128GB", "256GB", "512GB", "1TB"],
"اندازه صفحه": ["14 اینچ", "12 اینچ", "16 اینچ"],
"کارت گرافیک": ["NVIDIA", "AMD", "Intel", "Apple M1", "ARM Mali"],
"جنس پارچه": ["پنبه", "پلی‌استر", "نایلون", "ریون", "کتان"],
"نوع یقه": ["گرد", "هفت", "ایستاده", "دکمه‌ای", "یقه اسکی"],
"نوع آستین": ["کوتاه", "بلند", "سه‌ربع", "بی‌آستین", "کشباف"],
"مدل لباس": ["تی‌شرت", "پیراهن", "ژاکت", "هودی", "پلیور"],
"نوع شستشو": ["ماشینی", "دستی", "خشک‌شویی", "آب سرد", "آب گرم"],
"توان مصرفی": ["150W", "200W", "400W"],
"جنس بدنه": ["فلز", "پلاستیک", "چوب", "سرامیک", "کربن"],
ظرفیت: ["1 لیتر", "2 لیتر", "3 لیتر", "4 لیتر", "5 لیتر"],
"قابلیت تنظیم": ["بله", "خیر"],
"نوع کاربری": ["آبمیوه‌گیری", "مخلوط‌کن", "آسیاب", "کباب‌پز", "زودپز"],
// Add more attributes and their values as needed
};
export const seedCategories = async (logger: Logger) => {
try {
logger.info("Categories seeding started");
const colors = (await ColorModel.find()).map((color) => color._id);
const sizes = (await SizeModel.find()).map((size) => size._id);
const meterages = (await MeterageModel.find()).map((meterage) => meterage._id);
const createdCategories: { [key: string]: Types.ObjectId } = {};
const createdSubCategories: { [key: string]: Types.ObjectId } = {};
// Create root categories
for (const category of rootCategories) {
const { title_fa, icon, description, title_en, leaf, imageUrl } = category;
const rootCategory = new CategoryModel({ title_fa, icon, title_en, description, leaf, imageUrl });
await rootCategory.save();
createdCategories[title_fa] = rootCategory._id;
}
// Create subcategories
for (const [rootName, subs] of Object.entries(subcategories)) {
const parentId = createdCategories[rootName];
if (parentId) {
for (const subcategory of subs) {
const { title_fa, icon, description, title_en, leaf, theme, imageUrl } = subcategory;
const catVariants =
theme === CategoryThemeEnum.Colored
? [...colors]
: theme === CategoryThemeEnum.Meterage
? [...meterages]
: theme === CategoryThemeEnum.Sized
? [...sizes]
: [];
const subCategoryDoc = new CategoryModel({
title_fa,
icon,
imageUrl,
title_en,
description,
leaf,
theme,
variants: catVariants,
parent: parentId,
});
await subCategoryDoc.save();
// Save the subcategory ID for later use
createdSubCategories[title_fa] = subCategoryDoc._id;
}
}
}
const createdAttributes: { [key: string]: number } = {};
// Generate attributes for each subcategory
for (const [subcategoryTitle, attributes] of Object.entries(categoryAttributesMap)) {
const subcategoryId = createdSubCategories[subcategoryTitle];
if (subcategoryId) {
for (const attr of attributes) {
const attributeDoc = new CategoryAttributeModel({
category: subcategoryId,
title: attr.title,
hint: attr.hint,
type: attr.type,
multiple: attr.multiple,
required: attr.required,
});
await attributeDoc.save();
createdAttributes[attr.title] = attributeDoc._id;
}
} else {
logger.warn(`Subcategory '${subcategoryTitle}' not found.`);
}
}
// Generate values for each attribute
for (const [attributeTitle, attributeId] of Object.entries(createdAttributes)) {
const values = attributeValuesMap[attributeTitle] || [];
for (const valueText of values) {
await AttributeValueModel.create({
attribute: attributeId,
text: valueText,
});
// await valueDoc.save();
}
}
logger.info("Categories, subcategories, attributes, and values seeded successfully");
} catch (error) {
logger.error("Error seeding categories:", error);
}
};
+17
View File
@@ -0,0 +1,17 @@
import { Logger } from "../../core/logging/logger";
import { ContractModel } from "../../modules/admin/models/contract.model";
export const contractSeeder = async (logger: Logger) => {
try {
const existContract = await ContractModel.exists({});
if (existContract) return;
await ContractModel.create({
content: "متن قرار داد را اینجا قرار دهید",
});
logger.info("Contract collection seeded!");
} catch (error) {
logger.error("Contract collection seed failed!", error);
}
};
+68
View File
@@ -0,0 +1,68 @@
import { Logger } from "../../core/logging/logger";
import { PermissionEnum } from "../../modules/admin/models/Abstraction/IPermission";
import { RoleEnum } from "../../modules/admin/models/Abstraction/IRole";
import { AdminModel } from "../../modules/admin/models/admin.model";
import { PermissionModel } from "../../modules/admin/models/permission.model";
import { RoleModel } from "../../modules/admin/models/role.model";
import { ShipmentModel } from "../../modules/shipment/models/shipment.model";
import { OwnerRef } from "../../modules/shop/models/Abstraction/IShop";
import { ShopModel } from "../../modules/shop/models/shop.model";
const createDefaultAdmin = async (logger: Logger) => {
logger.info("createDefaultAdmin started", "SEED");
const email = process.env.DEFAULT_ADMIN_EMAIL;
const password = process.env.DEFAULT_ADMIN_PASSWORD;
if (!email || !password) {
throw new Error("Default admin credentials are not set in the environment variables");
}
// Create permissions based on the enum
const permissions = Object.values(PermissionEnum);
const createdPermissions = await Promise.all(
permissions.map(async (perm) => {
const existingPermission = await PermissionModel.findOne({ name: perm });
if (!existingPermission) {
return PermissionModel.create({ name: perm });
}
return existingPermission;
}),
);
// create or find the SUPERADMIN role
let superAdminRole = await RoleModel.findOne({ name: RoleEnum.SUPERADMIN });
if (!superAdminRole) {
superAdminRole = await RoleModel.create({
name: RoleEnum.SUPERADMIN,
});
logger.info("SUPERADMIN role created", "SEED");
} else {
logger.warn("SUPERADMIN role already exists", "SEED");
}
let admin = await AdminModel.findOne({ email });
if (!admin) {
admin = await AdminModel.create({
email,
password,
fullName: "defaultAdmin",
userName: "defaultAdmin",
permissions: createdPermissions.map((perm) => perm._id),
role: superAdminRole._id,
});
const shipments = (await ShipmentModel.find()).map((shipper) => shipper._id);
await ShopModel.create({
owner: admin._id.toString(),
ownerRef: OwnerRef.ADMIN,
shipmentMethod: shipments,
shopDescription: "فروشگاه شینان",
shopName: "شینان",
});
logger.info("Default admin user and native shop created", "SEED");
} else {
logger.warn("Default admin user and native shop already exists", "SEED");
}
};
export { createDefaultAdmin };
+36
View File
@@ -0,0 +1,36 @@
import { Logger } from "../../core/logging/logger";
import { FaqModel } from "../../modules/faq/models/faq.model";
export const seedFaqs = async (logger: Logger) => {
try {
logger.info("start seeding the FAQ");
const faqs = [
{
question: "چگونه میتوانم سفارش خود را پیگیری کنم؟",
answer: "برای پیگیری سفارش خود، می‌توانید به حساب کاربری خود وارد شوید و از قسمت سفارشات من، وضعیت سفارش خود را مشاهده کنید.",
},
{
question: "آیا امکان بازگشت کالا وجود دارد؟",
answer: "بله، در صورت داشتن شرایط بازگشت، می‌توانید کالا را تا ۷ روز پس از تحویل بازگردانید.",
},
{
question: "چگونه میتوانم حساب کاربری ایجاد کنم؟",
answer: "برای ایجاد حساب کاربری، کافیست در صفحه اصلی سایت به قسمت ثبت نام مراجعه کرده و اطلاعات خود را وارد کنید.",
},
{
question: "روش‌های پرداخت چگونه است؟",
answer: "پرداخت‌ها می‌تواند از طریق کارت‌های شتاب، درگاه‌های بانکی و پرداخت در محل انجام شود.",
},
{
question: "چگونه میتوانم از کد تخفیف استفاده کنم؟",
answer: "هنگام پرداخت، در قسمت کد تخفیف، کد خود را وارد کنید و سپس دکمه اعمال را بزنید.",
},
];
await FaqModel.insertMany(faqs);
logger.info("FAQ data seeded successfully");
} catch (error) {
logger.error("Error seeding FAQ data:", error);
}
};
+62
View File
@@ -0,0 +1,62 @@
import "reflect-metadata";
import { config } from "dotenv";
config();
import mongoose from "mongoose";
import { Logger } from "../../core/logging/logger";
import { connectMongo } from "../connection";
// import { backfillHierarchy } from "./backfillHierarchy";
import { blogCategorySeeder } from "./blogCategorySeeder";
import { blogSeeder } from "./blogSeeder";
import { contractSeeder } from "./contractSeeder";
// import { seedMedia } from "./mediaSeeder";
import { seedCityAndProvince } from "./iranCityseeder";
import { SeedPricing } from "./pricingSeeder";
import { clearDatabase } from "../clearDatabase";
import { backfillHierarchy } from "./backfillHierarchy";
import { createDefaultAdmin } from "./defaultAdmin";
import { seedFaqs } from "./faqSeeder";
import { seedMedia } from "./mediaSeeder";
import { seedPaymentMethods } from "./pgSeeder";
import { ReportQuesSeeder } from "./reportQuestion";
import { createDefaultRoles } from "./roleSeeder";
import { SeedShipment } from "./shipmentSeeder";
import { seedTicketCategories } from "./ticketSeeder";
import { seedWarranty } from "./warrantySeeders";
const logger = new Logger("Seeder");
const seedDatabase = async () => {
try {
await connectMongo();
logger.info("connected to mongodb");
await clearDatabase(logger);
await backfillHierarchy();
await blogCategorySeeder(logger);
await blogSeeder(logger);
await contractSeeder(logger);
await createDefaultAdmin(logger);
await seedFaqs(logger);
await seedCityAndProvince(logger);
await SeedPricing(logger);
await seedMedia(logger);
await seedPaymentMethods(logger);
await ReportQuesSeeder(logger);
await createDefaultRoles(logger);
await SeedShipment(logger);
await seedTicketCategories(logger);
await seedWarranty(logger);
logger.info("Seeding completed");
process.exit(0);
} catch (error) {
//
console.error("Error during seeding:", error);
logger.error("Error during seeding:", error);
process.exit(1);
} finally {
//
await mongoose.connection.close();
}
};
seedDatabase();
+39
View File
@@ -0,0 +1,39 @@
import { Logger } from "../../core/logging/logger";
import { CityModel } from "../../modules/address/models/city.model";
import { ProvinceModel } from "../../modules/address/models/province.model";
import province from "../data/json/ostan.json";
import city from "../data/json/shahr.json";
interface IProvince {
id: number;
name: string;
}
interface ICities extends IProvince {
ostan: number;
}
const provinces: IProvince[] = province;
const cities: ICities[] = city;
export const seedCityAndProvince = async (logger: Logger) => {
try {
logger.info("start seeding the city and province", "SEED");
for (const province of provinces) {
await ProvinceModel.create({
name: province.name,
});
}
for (const city of cities) {
await CityModel.create({
name: city.name,
province: city.ostan,
});
}
logger.info("city and province seeding completed", "SEED");
} catch (error) {
logger.error("error in seeding the city and province", error);
}
};
+46
View File
@@ -0,0 +1,46 @@
import { Logger } from "../../core/logging/logger";
import { JobType } from "../../modules/job/models/Abstraction/IJob";
import { JobModel } from "../../modules/job/models/job.model";
export const seedJobs = async (logger: Logger) => {
const jobs = [
{
title: "برنامه‌نویس وب",
description: "ما به دنبال یک برنامه‌نویس وب با تجربه در React و Node.js هستیم.",
location: "تهران",
type: JobType.FullTime,
},
{
title: "طراح UI/UX",
description: "نیازمند یک طراح UI/UX با تجربه برای بهبود تجربه کاربری وب‌سایت هستیم.",
location: "اصفهان",
type: JobType.Contract,
},
{
title: "کارشناس فروش",
description: "به یک کارشناس فروش برای توسعه بازار و جذب مشتری نیازمندیم.",
location: "شیراز",
type: JobType.PartTime,
},
{
title: "مدیر پروژه",
description: "نیازمند مدیر پروژه‌ای با تجربه در مدیریت تیم‌های فنی برای مدیریت پروژه‌های IT.",
location: "مشهد",
type: JobType.FullTime,
},
{
title: "تحلیل‌گر داده",
description: "به یک تحلیل‌گر داده برای تحلیل و بررسی داده‌های شرکت نیاز داریم.",
location: "تبریز",
type: JobType.Contract,
},
];
try {
logger.info("start seeding the jobs");
await JobModel.insertMany(jobs);
logger.info("Job seeding completed!");
} catch (error) {
logger.error("Job seeding failed: ", error);
}
};
+193
View File
@@ -0,0 +1,193 @@
import { Logger } from "../../core/logging/logger";
import { BannerModel } from "../../modules/admin/models/media.model";
export async function seedMedia(logger: Logger) {
try {
// await SliderModel.deleteMany({});
// await BannerModel.deleteMany({});
// logger.info("Cleared existing data");
// // Seed Sliders
// const sliders = [
// {
// displayLocation: DisplayLocations.Mobile,
// imageUrl: "https://loremflickr.com/1920/1080",
// altText: "اسلایدر ۱",
// title: "خوش آمدید به اسلایدر ۱",
// linkUrl: "/slider1",
// isActive: true,
// },
// {
// displayLocation: DisplayLocations.Mobile,
// imageUrl: "https://loremflickr.com/1920/1080",
// altText: "اسلایدر ۲",
// title: "اسلایدر ۲ را کشف کنید",
// linkUrl: "/slider2",
// isActive: true,
// },
// {
// displayLocation: DisplayLocations.Desktop,
// imageUrl: "https://loremflickr.com/1920/1080",
// altText: "اسلایدر ۳",
// title: "بیشتر درباره اسلایدر ۳ بدانید",
// linkUrl: "/slider3",
// isActive: true,
// },
// {
// displayLocation: DisplayLocations.Desktop,
// imageUrl: "https://loremflickr.com/1920/1080",
// altText: "اسلایدر ۴",
// title: "اسلایدر جدید و جذاب",
// linkUrl: "/slider4",
// isActive: true,
// },
// ];
// for (const slider of sliders) {
// await SliderModel.create(slider);
// }
// logger.info(" Sliders Seeded");
const existingBanners = await BannerModel.countDocuments({});
if (existingBanners && existingBanners == 16) return;
await BannerModel.deleteMany({});
const banners = [
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر ۱",
title: "خوش آمدید به صفحه اصلی",
linkUrl: "/banner1",
isActive: true,
order: 1,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر ۲",
title: "درباره ما بیشتر بدانید",
linkUrl: "/banner2",
isActive: true,
order: 2,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر ۳",
title: "تماس با ما",
linkUrl: "/banner3",
isActive: true,
order: 3,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر ۴",
title: "خدمات ما را کشف کنید",
linkUrl: "/banner4",
isActive: true,
order: 4,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر ۵",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner5",
isActive: true,
order: 5,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 6",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner6",
isActive: true,
order: 6,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 7",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner7",
isActive: true,
order: 7,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 8",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner8",
isActive: true,
order: 8,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 9",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner9",
isActive: true,
order: 9,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 10",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner10",
isActive: true,
order: 10,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 11",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner11",
isActive: true,
order: 11,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 12",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner12",
isActive: true,
order: 12,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 13",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner13",
isActive: true,
order: 13,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 14",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner14",
isActive: true,
order: 14,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 15",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner15",
isActive: true,
order: 15,
},
{
imageUrl: "https://storage.shinan.ir/banner-placeholder.png",
altText: "بنر 16",
title: "محصولات ما را مشاهده کنید",
linkUrl: "/banner16",
isActive: true,
order: 16,
},
];
await BannerModel.insertMany(banners);
logger.info("Banners seeding completed!");
} catch (error) {
logger.error("Error seeding the Medias: ", error);
}
}
+50
View File
@@ -0,0 +1,50 @@
import { PaymentMethodType } from "../../common/enums/payment.enum";
import { Logger } from "../../core/logging/logger";
import { PaymentMethodModel } from "../../modules/payment/models/paymentMethod.model";
// Define payment method data
const paymentMethods = [
{
title_fa: "پرداخت اینترنتی",
title_en: "Internet Payment Gateway (IPG)",
description: "پرداخت آنلاین با تمامی کارت‌های بانکی",
provider: "Zarinpal",
isActive: true,
type: PaymentMethodType.Online,
},
{
title_fa: "پرداخت اینترنتی",
title_en: "Internet Payment Gateway (IPG)",
description: "پرداخت آنلاین با تمامی کارت‌های بانکی",
provider: "asanPardakht",
isActive: false,
type: PaymentMethodType.Online,
},
{
title_fa: "پرداخت در محل (با کارت بانکی)",
title_en: "POS",
description: "هنگام تحویل از طریق کارت‌های بانکی",
isActive: false,
type: PaymentMethodType.POS,
},
{
title_fa: "کیف پول",
title_en: "Wallet",
isActive: false,
type: PaymentMethodType.Wallet,
},
// Add more payment methods as necessary
];
// Seeder function to insert the payment methods
export const seedPaymentMethods = async (logger: Logger) => {
try {
logger.info("PaymentMethods seeding started");
await PaymentMethodModel.insertMany(paymentMethods);
logger.info("Payment methods seeded successfully");
} catch (error) {
logger.error("Error seeding payment methods:", error);
}
};
+27
View File
@@ -0,0 +1,27 @@
import { Logger } from "../../core/logging/logger";
import { IPricing, PricingTypeEnum } from "../../modules/pricing/models/Abstraction/IPricing";
import { PricingModel } from "../../modules/pricing/models/pricing.model";
export const SeedPricing = async (logger: Logger) => {
const pricing: IPricing[] = [
{ type: PricingTypeEnum.INSERT_FEE, price: 100_000 },
{ type: PricingTypeEnum.PHOTOGRAPHY_FEE, price: 150_000 },
{ type: PricingTypeEnum.UNBOXING_VIDEO_FEE, price: 200_000 },
{ type: PricingTypeEnum.EXPERT_REVIEWS_FEE, price: 200_000 },
];
try {
logger.info("Pricing seeding started");
const existPricing = await PricingModel.countDocuments({});
if (existPricing && existPricing == 4) return;
await PricingModel.deleteMany({});
await PricingModel.insertMany(pricing);
logger.info("Pricing seeded successfully");
} catch (error) {
logger.error("Failed to Pricing:", error);
}
};
+325
View File
@@ -0,0 +1,325 @@
import { faker } from "@faker-js/faker";
faker.locale = "fa";
import { ProductMarketStatus } from "../../common/enums/product.enum";
import { Logger } from "../../core/logging/logger";
import { BrandModel } from "../../modules/brand/models/brand.model";
import { AttributeValueModel } from "../../modules/category/models/attributeValue.model";
import { CategoryModel } from "../../modules/category/models/category.model";
import { CategoryAttributeModel } from "../../modules/category/models/CategoryAttribute.model";
import { ColorModel } from "../../modules/category/models/color.model";
import { MeterageModel } from "../../modules/category/models/meterage.model";
import { SizeModel } from "../../modules/category/models/size.model";
import { ISpecifications } from "../../modules/product/models/Abstraction/IProduct";
import { ProductModel } from "../../modules/product/models/product.model";
import { ProductVariantModel } from "../../modules/product/models/productVariant.model";
import { SellerModel } from "../../modules/seller/models/seller.model";
import { ShipmentModel } from "../../modules/shipment/models/shipment.model";
import { ShopModel } from "../../modules/shop/models/shop.model";
import { WalletModel } from "../../modules/wallet/models/wallet.model";
import { WarrantyModel } from "../../modules/warranty/models/warranty.model";
import { TimeService } from "../../utils/time.service";
import { CreateFakeSellerData, createFakeShopData, fakeProductsData } from "../data/product";
type ProductCategory =
| "mobile"
| "stationery"
| "electric-electrical"
| "tools-equipment"
| "audio-visual"
| "computer"
| "game-console"
| "sports-travel"
| "fashion"
| "home-appliances"
| "mobile-phones"
| "tablets"
| "pens"
| "paper-notebooks"
| "bulbs"
| "cables-wires"
| "wrenches-screwdrivers"
| "drills"
| "televisions"
| "speakers"
| "laptops"
| "mouse-keyboards"
| "playstations"
| "xbox"
| "sports-equipment"
| "cameras"
| "headphones"
| "travel-equipment"
| "women-clothing"
| "men-clothing"
| "kitchen-appliances"
| "smart-watch"
| "power-banks"
| "cleaning-appliances"
| "malicious";
type ProductBrand =
| "Samsung"
| "LG"
| "Bosch"
| "Apple"
| "Asus"
| "Xiaomi"
| "Sony"
| "JBL"
| "Bose"
| "Huawei"
| "Nokia"
| "Nikon"
| "Panasonic"
| "Anker"
| "Microsoft"
| "Lenovo"
| "Pilot"
| "Sheaffer"
| "Parker"
| "Lamy"
| "Parker"
| "Philips"
| "Optonique"
| "NoorKala"
| "Acer"
| "Zara"
| "H&M"
| "Gucci"
| "Dior"
| "Gucci"
| "Gucci"
| "malicious";
const productMapping = (productModel: string) => {
let category: ProductCategory;
let brand: ProductBrand;
switch (productModel) {
case "SAMSUNG123":
case "SAMGALTAB567":
brand = "Samsung";
category = productModel === "SAMSUNG123" ? "televisions" : "tablets";
break;
case "LGWASH456":
brand = "LG";
category = "kitchen-appliances";
break;
case "LGOLED789":
brand = "LG";
category = "televisions";
break;
case "BOSCHFRIDGE789":
brand = "Bosch";
category = "kitchen-appliances";
break;
case "IPHONE13PRO":
case "APPLEWATCH123":
brand = "Apple";
category = productModel === "IPHONE13PRO" ? "mobile-phones" : "smart-watch";
break;
case "ASUSLAP234":
brand = "Asus";
category = "laptops";
break;
case "MI11PRO":
brand = "Xiaomi";
category = "mobile-phones";
break;
case "SONYWH123":
case "SONYCAM456":
case "SONYSPK234":
brand = "Sony";
category = productModel === "SONYWH123" ? "headphones" : productModel === "SONYCAM456" ? "cameras" : "speakers";
break;
case "JBLSPK789":
brand = "JBL";
category = "speakers";
break;
case "NIKONCAM123":
brand = "Nikon";
category = "cameras";
break;
case "BOSEWH789":
brand = "Bose";
category = "headphones";
break;
case "HUAWEITAB1":
brand = "Huawei";
category = "tablets";
break;
case "NOKIAG20":
brand = "Nokia";
category = "mobile-phones";
break;
case "PANAMW234":
brand = "Panasonic";
category = "kitchen-appliances";
break;
case "ANKERPB100":
brand = "Anker";
category = "power-banks";
break;
case "PS5DISC":
brand = "Sony";
category = "playstations";
break;
default:
brand = "malicious";
category = "malicious";
}
return { brand, category };
};
const getBrandAndCategoryForProduct = async (product: (typeof fakeProductsData)[0]) => {
const { brand: brandName, category: categoryName } = productMapping(product.model) || {};
const category = await CategoryModel.findOne({ title_en: categoryName });
if (!category) {
throw new Error(`Category not found for product: ${product.title_fa}`);
}
const brand = await BrandModel.findOne({ title_en: brandName, category: category._id.toString() });
if (!brand) {
throw new Error(`Brand not found for product: ${product.title_fa}`);
}
return { brand, category };
};
const createFakeProductVariant = (
productId: number,
shopId: string,
warrantyId: number,
colorId?: number,
sizeId?: number,
meterageId?: number,
) => {
return new ProductVariantModel({
product: productId,
rate: faker.datatype.float({ min: 1, max: 5 }),
warranty: warrantyId,
shop: shopId,
color: colorId,
size: sizeId,
meterage: meterageId,
sellerSpecialCode: crypto.randomUUID().slice(0, 6).toUpperCase(),
market_status: faker.helpers.arrayElement([ProductMarketStatus.Marketable]),
postingTime: faker.datatype.number({ min: 1, max: 10 }),
price: {
order_limit: faker.datatype.number({ min: 1, max: 10 }),
retailPrice: faker.datatype.number({ min: 10000, max: 100000000 }),
is_specialSale: true,
discount_percent: faker.datatype.number({ min: 1, max: 50 }),
specialSale_order_limit: faker.datatype.number({ min: 1, max: 5 }),
specialSale_quantity: faker.datatype.number({ min: 1, max: 5 }),
specialSale_endDate: TimeService.convertGregorianToPersian(faker.date.future()),
},
saleFormat: {
retail: true,
wholeSale: [
{ from: 10, to: 30, price: faker.datatype.number({ min: 10000, max: 100000000 }) },
{ from: 30, to: 50, price: faker.datatype.number({ min: 10000, max: 100000000 }) },
{ from: 50, to: 100, price: faker.datatype.number({ min: 10000, max: 100000000 }) },
],
},
stock: faker.datatype.number({ min: 20, max: 100 }),
dimensions: {
package_weight: faker.datatype.number({ min: 1, max: 5 }),
package_height: faker.datatype.number({ min: 10, max: 100 }),
package_length: faker.datatype.number({ min: 10, max: 100 }),
package_width: faker.datatype.number({ min: 10, max: 100 }),
},
});
};
export const seedProduct = async (logger: Logger) => {
try {
logger.info("start seeding products");
// Fetch warranties and shipment methods
const warranties = await WarrantyModel.find().limit(10);
const shipments = (await ShipmentModel.find()).map((shipper) => shipper._id);
const colors = (await ColorModel.find()).map((color) => color._id);
const sizes = (await SizeModel.find()).map((size) => size._id);
const meterages = (await MeterageModel.find()).map((meterage) => meterage._id);
if (!warranties.length) {
throw new Error("No warranties found in the database. Please seed them first.");
}
const remainingProducts = [...fakeProductsData];
for (let i = 0; i < 10; i++) {
// Create fake seller
const seller = await SellerModel.create(CreateFakeSellerData());
await WalletModel.create({ seller: seller._id.toString() });
const shop = await ShopModel.create(createFakeShopData(faker.helpers.arrayElements(shipments, 3), seller._id.toString()));
const sellerProducts = remainingProducts.splice(0, 5);
for (const productData of sellerProducts) {
// Get the correct brand and category for the product
const { brand, category } = await getBrandAndCategoryForProduct(productData);
const catAttributes = await CategoryAttributeModel.find({ category: category._id.toString() });
const attributeValues = await AttributeValueModel.find({ attribute: { $in: catAttributes.map((att) => att._id) } });
const specifications: ISpecifications[] = catAttributes.map((catAttr) => {
const valuesForAttribute = attributeValues
.filter((attValue) => attValue.attribute.toString() === catAttr._id.toString())
.map((attValue) => attValue.text);
return {
title: catAttr.title,
values: valuesForAttribute,
};
});
const product = await new ProductModel({
...productData,
shop: shop._id,
specifications,
brand: brand._id.toString(),
category: category._id.toString(),
}).save();
for (let k = 0; k < 3; k++) {
const variant = await createFakeProductVariant(
product._id,
shop._id.toString(),
faker.helpers.arrayElement(warranties)._id,
productData.color ? faker.helpers.arrayElement(colors) : undefined,
productData.size ? faker.helpers.arrayElement(sizes) : undefined,
productData.meterage ? faker.helpers.arrayElement(meterages) : undefined,
).save();
product.variants.push(variant._id);
await product.save();
}
}
if (remainingProducts.length < 5) break;
}
logger.info("Seeding product completed!");
} catch (error) {
console.error(error);
logger.error("Failed to seed product and variant:", error);
}
};
+61
View File
@@ -0,0 +1,61 @@
import { Logger } from "../../core/logging/logger";
import { ReportQuestionModel } from "../../modules/product/models/reportQuestion.model";
const Questions = [
{
title: "نام کالا نادرست است",
type: "checkbox",
},
{
title: "عکس کالا نامناسب است",
type: "checkbox",
},
{
title: "مشخصات کالا نادرست است",
type: "checkbox",
},
{
title: "توضیحات کالا نادرست است",
type: "checkbox",
},
{
title: "کالا غیراصل است",
type: "checkbox",
},
{
title: "کالا ناقض قوانین جمهوری اسلامی است",
type: "checkbox",
},
{
title: "کالا تکراری است",
type: "checkbox",
// related_questions: [relatedQues._id],
},
];
export const ReportQuesSeeder = async (logger: Logger) => {
try {
logger.info("ReportQuestion seeding started");
await ReportQuestionModel.deleteMany();
for (const question of Questions) {
await ReportQuestionModel.create(question);
}
// const relatedQues = await ReportQuestionModel.create({
// title: "کد کالای مشابه ",
// type: "number",
// placeholder: "https://shinan.ir/product/SHP-313420/",
// });
// await ReportQuestionModel.create({
// title: "کالا تکراری است",
// type: "checkbox",
// related_questions: [relatedQues._id],
// });
logger.info("report questions seeded successfully");
} catch (error) {
logger.error("error seeding the report questions:", error);
}
};
+36
View File
@@ -0,0 +1,36 @@
import { Logger } from "../../core/logging/logger";
import { RoleEnum } from "../../modules/admin/models/Abstraction/IRole";
import { RoleModel } from "../../modules/admin/models/role.model";
const createDefaultRoles = async (logger: Logger) => {
logger.info("create roles started", "SEED");
// const roles = Object.values(RoleEnum);
const rolesWithPermissions = [
{
role: RoleEnum.SUPERADMIN,
},
{
role: RoleEnum.MODERATOR,
},
{
role: RoleEnum.EDITOR,
},
];
const createdRoles = await Promise.all(
rolesWithPermissions.map(async ({ role }) => {
const existingRole = await RoleModel.findOne({ name: role });
if (!existingRole) {
return RoleModel.create({ name: role });
}
return existingRole;
}),
);
if (createdRoles) {
logger.info("Default roles created", "SEED");
} else {
logger.warn("Default roles already exists", "SEED");
}
};
export { createDefaultRoles };
+75
View File
@@ -0,0 +1,75 @@
import { DeliveryType } from "../../common/enums/shipment.enum";
import { Logger } from "../../core/logging/logger";
import { ShipmentModel } from "../../modules/shipment/models/shipment.model";
export const SeedShipment = async (logger: Logger) => {
const providers = [
{
name: "پست",
description: "ارسال توسط پست",
costs: [
{ weightRange: { min: 0, max: 1 }, cost_first: 40000, cost_rest: 2000 },
{ weightRange: { min: 1, max: 5 }, cost_first: 80000, cost_rest: 2000 },
{ weightRange: { min: 5, max: 10 }, cost_first: 100000, cost_rest: 2000 },
],
deliveryTime: 5, // Standard (3-5 days)
deliveryType: DeliveryType.Standard,
},
{
name: "چاپار",
description: "ارائه بهترین راه حل های جهانی لجستیک چاپار",
costs: [
{ weightRange: { min: 0, max: 2 }, cost_first: 40000, cost_rest: 2000 },
{ weightRange: { min: 2, max: 6 }, cost_first: 80000, cost_rest: 2000 },
{ weightRange: { min: 6, max: 12 }, cost_first: 100000, cost_rest: 2000 },
],
deliveryTime: 6, // Standard (4-6 days)
deliveryType: DeliveryType.Standard,
},
{
name: "پیک",
description: "ارسال توسط پیک",
costs: [
{ weightRange: { min: 0, max: 1 }, cost_first: 40000, cost_rest: 2000 },
{ weightRange: { min: 1, max: 3 }, cost_first: 80000, cost_rest: 2000 },
{ weightRange: { min: 3, max: 6 }, cost_first: 100000, cost_rest: 2000 },
],
deliveryTime: 0, // Same Day
deliveryType: DeliveryType.SameDay,
},
{
name: "تیپاکس",
description: "ارسال بسته های پستی در سریعترین زمان، از درب منزل با امکان پرداخت هزینه بعد از ارسال و پوشش تمام نقاط ایران",
costs: [
{ weightRange: { min: 0, max: 1 }, cost_first: 40000, cost_rest: 2000 },
{ weightRange: { min: 1, max: 5 }, cost_first: 80000, cost_rest: 2000 },
{ weightRange: { min: 5, max: 10 }, cost_first: 100000, cost_rest: 2000 },
],
deliveryTime: 2, // Express (1-2 days)
deliveryType: DeliveryType.Express,
},
{
name: "باربری",
description: "ارسال توسط باربری",
costs: [
{ weightRange: { min: 0, max: 10 }, cost_first: 40000, cost_rest: 2000 },
{ weightRange: { min: 10, max: 20 }, cost_first: 80000, cost_rest: 2000 },
{ weightRange: { min: 20, max: 50 }, cost_first: 100000, cost_rest: 2000 },
],
deliveryTime: 7, // Freight (5-7 days)
deliveryType: DeliveryType.Standard,
},
];
try {
logger.info("ShipmentMethod seeding started");
for (const provider of providers) {
await ShipmentModel.create(provider);
}
logger.info("Shipment Providers seeded successfully");
} catch (error) {
logger.error("Error seeding Shipment providers:", error);
}
};
+38
View File
@@ -0,0 +1,38 @@
import { Logger } from "../../core/logging/logger";
import { TicketCategoryModel } from "../../modules/ticket/models/ticketCategory.model";
// Define ticket categories to seed
const ticketCategories = [
{
title_fa: "مشکل فنی",
description: "مشکلات مربوط به سیستم، سایت یا فروشگاه.",
},
{
title_fa: "مشکلات مالی",
description: "مشکلات مربوط به پرداخت‌ها و مسائل مالی.",
},
{
title_fa: "پشتیبانی محصول",
description: "سوالات و پشتیبانی‌های مربوط به محصولات.",
},
{
title_fa: "مشکلات ارسال",
description: "مشکلات مربوط به ارسال سفارشات و حمل و نقل.",
},
{
title_fa: "درخواست راهنمایی",
description: "درخواست‌ها برای راهنمایی و کمک در مورد مسائل مختلف.",
},
];
export const seedTicketCategories = async (logger: Logger) => {
try {
logger.info("start seeding the ticket category");
await TicketCategoryModel.insertMany(ticketCategories);
logger.info("Ticket categories seeded successfully!");
} catch (error) {
logger.error("Failed to seed ticket categories:", error);
}
};
+47
View File
@@ -0,0 +1,47 @@
import { faker } from "@faker-js/faker"; // Use faker for generating random data
import { StatusEnum } from "../../common/enums/status.enum";
import { Logger } from "../../core/logging/logger";
import { WarrantyModel } from "../../modules/warranty/models/warranty.model";
const warrantyNames = [
"Iran Warranty Co.",
"Pars Guarantee",
"Tehran Warranty Services",
"Safir Warranty",
"Farda Warranty Solutions",
"Aryan Protection",
"Mehr Warranty",
"Saman Warranty Group",
"Persian Warranty Solutions",
"Shahab After Sales",
"Payam Warranty",
"Kavir Protection",
"Tajrish Warranty",
"Sina Warranty",
"Atlas Warranty Services",
"Sepahan Warranty",
"Kish Warranty Group",
"Navid Warranty Solutions",
"Alborz Protection Services",
"Raha Warranty Solutions",
];
export const seedWarranty = async (logger: Logger) => {
try {
logger.info("Warranty seeding started");
for (const name of warrantyNames) {
await WarrantyModel.create({
name,
status: StatusEnum.Approved,
duration: faker.datatype.number({ min: 1, max: 36 }) + " months",
logoUrl: faker.image.business(),
});
}
logger.info("Warranty seeding complete!");
} catch (error) {
logger.error("Error seeding warranties:", error);
}
};
+156
View File
@@ -0,0 +1,156 @@
import { EventEmitter } from "events";
import { HydratedDocument, Types } from "mongoose";
import { ColorModel } from "../modules/category/models/color.model";
import { MeterageModel } from "../modules/category/models/meterage.model";
import { SizeModel } from "../modules/category/models/size.model";
import { IPriceHistory } from "../modules/product/models/Abstraction/IPriceHistory";
import { PriceHistoryModel } from "../modules/product/models/priceHistory.model";
import { ProductObserveModel } from "../modules/product/models/productObserve.model";
import { IUser } from "../modules/user/models/Abstraction/IUser";
import { EmailService } from "../utils/email.service";
import { TimeService } from "../utils/time.service"; // Use DI to inject TimeService for better testability
enum PriceChangeEventType {
PRICE_CHANGE = "priceChange",
OBSERVER_NOTIFICATION = "observerNotification",
}
type PriceDetails = {
product: number;
shop: string;
selling_price: number;
retail_price: number;
variantId: string;
isSpecial?: boolean;
colorId?: number;
sizeId?: number;
meterageId?: number;
};
type historyDetail = { shop: string; selling_price: number; retail_price: number };
class PriceChangeEventService extends EventEmitter {
private static instance: PriceChangeEventService;
private constructor() {
super();
this.subscribeToEvents();
}
//**** */
public static getInstance(): PriceChangeEventService {
if (!PriceChangeEventService.instance) {
PriceChangeEventService.instance = new PriceChangeEventService();
}
return PriceChangeEventService.instance;
}
//**** */
public emitPriceChange(priceDetails: PriceDetails): void {
this.emit(PriceChangeEventType.PRICE_CHANGE, priceDetails);
if (priceDetails.isSpecial) {
this.emitObserverNotification(priceDetails);
}
}
//**** */
private emitObserverNotification(priceDetails: PriceDetails): void {
this.emit(PriceChangeEventType.OBSERVER_NOTIFICATION, priceDetails);
}
//**** */
private subscribeToEvents(): void {
this.on(PriceChangeEventType.PRICE_CHANGE, this.handlePriceChange);
this.on(PriceChangeEventType.OBSERVER_NOTIFICATION, this.handleObserverNotification);
}
//**** */
private async handlePriceChange(priceDetails: PriceDetails): Promise<void> {
try {
const title = await this.findVariantTitle(priceDetails);
const now = TimeService.getCurrentPersianDate();
const { colorId, sizeId, meterageId, product, ...history } = priceDetails;
const existingHistory = await PriceHistoryModel.findOne({ title, product });
if (existingHistory) {
await this.appendPriceHistory(existingHistory, history, now);
console.log(`Updated price history for product ${product} with price: ${history.selling_price}`);
} else {
await this.createPriceHistory(title, product, history, now);
console.log(`Created new price history for product ${product} with price: ${history.selling_price}`);
}
} catch (error) {
console.error("Failed to record price change:", error);
}
}
//**** */
private async handleObserverNotification(priceDetails: PriceDetails): Promise<void> {
try {
const observers = await this.findObserversForProduct(priceDetails.product, priceDetails.variantId);
for (const observer of observers) {
const user = observer.user as unknown as IUser;
const emailContent = this.generateEmailContent(user.fullName, observer.registeredPrice, priceDetails);
await EmailService.sendEmail({
subject: "تخفیف ویژه",
to: user.email,
text: emailContent,
});
console.log(`Sent notification to observer: ${user.email}`);
}
} catch (error) {
console.error("Failed to send observer notifications:", error);
}
}
//**** */
private async findObserversForProduct(productId: number, variantId: string) {
return await ProductObserveModel.find({ product: productId, variant: variantId }).populate("user").lean();
}
//**** */
private generateEmailContent(userName: string, observerPrice: number, priceDetails: PriceDetails): string {
const priceDiff = observerPrice - priceDetails.selling_price;
const message = `کاربر گرامی ${userName},\n\nقیمت محصولی که شما برای آن منتظر شده‌اید، به ${priceDetails.selling_price} تغییر کرده است.`;
if (priceDiff > 0) {
return message + `\n\nاین قیمت ${priceDiff} تومان کمتر از قیمت هدف شما است. این فرصت را از دست ندهید!`;
} else {
return message + `\n\nاین یک پیشنهاد ویژه است که ممکن است برای شما جذاب باشد!`;
}
}
//**** */
private async appendPriceHistory(existHistory: HydratedDocument<IPriceHistory>, history: historyDetail, now: string) {
existHistory.history.push({
...history,
date: now,
shop: new Types.ObjectId(history.shop),
});
await existHistory.save();
}
//**** */
private async createPriceHistory(title: string, product: number, history: historyDetail, now: string): Promise<void> {
const priceHistory = new PriceHistoryModel({ title, product });
priceHistory.history.push({
...history,
date: now,
shop: new Types.ObjectId(history.shop),
});
await priceHistory.save();
}
//**** */
private async findVariantTitle(priceDetails: PriceDetails): Promise<string> {
if (priceDetails.colorId) {
const color = await ColorModel.findById(priceDetails.colorId).lean();
return color?.name || "Unknown color";
} else if (priceDetails.sizeId) {
const size = await SizeModel.findById(priceDetails.sizeId).lean();
return size?.value || "Unknown size";
} else if (priceDetails.meterageId) {
const meterage = await MeterageModel.findById(priceDetails.meterageId).lean();
return meterage?.value || "Unknown meterage";
}
return "";
}
}
const instance = PriceChangeEventService.getInstance();
export { instance as PriceChangeEvent, PriceChangeEventService, PriceChangeEventType };
@@ -0,0 +1,84 @@
import { Expose } from "class-transformer";
import { IsArray, IsBoolean, IsMongoId, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidPersianDate } from "../../../common/decorator/validation.decorator";
export class CreateCouponDTO {
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 10, description: "Discount percentage for the coupon" })
discountPercentage?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 10000, description: "Flat discount amount" })
discountAmount?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon" })
usageLimit: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon per user" })
userUsageLimit: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 100000, description: "Minimum purchase amount to use the coupon in toman" })
minPurchaseAmount: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsMongoId({ each: true })
@ApiProperty({ type: "Array", example: ["60d21b4967d0d8992e610c85"], description: "categories for the coupon" })
category?: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of included product IDs" })
includedProducts?: number[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of excluded product IDs" })
excludedProducts?: number[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", example: "کد تخفیف", description: "Description of the coupon" })
description: string;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({ type: "boolean", example: false, description: "Whether the coupon applies to special sales" })
includeSpecialSale: boolean;
@Expose()
@IsNotEmpty()
@IsValidPersianDate()
@ApiProperty({ type: "string", example: "1401/06/05", description: "Coupon expiration date in jalali format" })
expirationDate: string;
}
@@ -0,0 +1,63 @@
import { Expose } from "class-transformer";
import { IsArray, IsMongoId, IsNotEmpty, IsNumber, IsOptional } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
export class UpdateCouponDTO {
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 10, description: "Discount percentage for the coupon" })
discountPercentage?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 10000, description: "Flat discount amount" })
discountAmount?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon" })
usageLimit?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 5, description: "Maximum number of uses for the coupon per user" })
userUsageLimit?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsNumber()
@ApiProperty({ type: "number", example: 100000, description: "Minimum purchase amount to use the coupon in toman" })
minPurchaseAmount?: number;
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@IsMongoId({ each: true })
@ApiProperty({ type: "Array", example: ["60d21b4967d0d8992e610c85"], description: "categories for the coupon" })
category?: string[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of included product IDs" })
includedProducts?: number[];
@Expose()
@IsOptional()
@IsNotEmpty()
@IsArray()
@ApiProperty({ type: "Array", example: [100001, 100102], description: "List of excluded product IDs" })
excludedProducts?: number[];
}
@@ -0,0 +1,20 @@
import { Expose } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { CartShipmentItemModel } from "../../cart/models/cartShipmentItem.model";
export class ValidateCouponDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", example: "DISCOUNT2024" })
code: string;
@Expose()
@IsNotEmpty()
@IsValidId(CartShipmentItemModel)
@ApiProperty({ type: "string", example: "5f7b1b1b1b1b1b1b1b1b1b1b1", description: "cartShipment item id" })
cartShipmentItemId: string;
}
+96
View File
@@ -0,0 +1,96 @@
import { Request } from "express";
import rateLimit from "express-rate-limit";
import { inject } from "inversify";
import { controller, httpGet, httpPatch, httpPost, request, requestBody, requestParam } from "inversify-express-utils";
import { CouponService } from "./coupon.service";
import { HttpStatus } from "../../common";
import { CreateCouponDTO } from "./DTO/createCoupon.dto";
import { UpdateCouponDTO } from "./DTO/updateCoupon.dto";
import { ValidateCouponDTO } from "./DTO/validateCoupon.dto";
import { BaseController } from "../../common/base/controller";
import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
import { appConfig } from "../../core/config/app.config";
import { Guard } from "../../core/middlewares/guard.middleware";
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
import { IOCTYPES } from "../../IOC/ioc.types";
import { ISeller } from "../seller/models/Abstraction/ISeller";
import { OwnerRef } from "../shop/models/Abstraction/IShop";
import { IUser } from "../user/models/Abstraction/IUser";
@controller("/coupon")
@ApiTags("Coupon")
class CouponController extends BaseController {
@inject(IOCTYPES.CouponService) couponService: CouponService;
@ApiOperation("get all seller created coupon ==> login as seller")
@ApiResponse("successful")
@ApiAuth()
@httpGet("", Guard.authSeller())
public async getSellerCoupons(@request() req: Request) {
const seller = req.user as ISeller;
const data = await this.couponService.getSellerCoupons(seller._id.toString(), OwnerRef.SELLER);
return this.response(data);
}
@ApiOperation("get a single coupon")
@ApiParam("id", "coupon id", true)
@ApiAuth()
@httpGet("/:id", Guard.authSeller())
public async getSingleCoupon(@requestParam("id") couponId: string) {
const data = await this.couponService.getSingleCoupon(couponId);
return this.response(data);
}
@ApiOperation("create a Coupon ==> login as seller")
@ApiResponse("successful", HttpStatus.Created)
@ApiModel(CreateCouponDTO)
@ApiAuth()
@httpPost("", rateLimit(appConfig.rate), Guard.authSeller(), ValidationMiddleware.validateInput(CreateCouponDTO))
public async createCoupon(@requestBody() createDto: CreateCouponDTO, @request() req: Request) {
const seller = req.user as ISeller;
const data = await this.couponService.createCoupon(createDto, seller._id.toString(), OwnerRef.SELLER);
return this.response(data, HttpStatus.Created);
}
@ApiOperation("deactivate a coupon ==> login as seller")
@ApiResponse("successful")
@ApiAuth()
@ApiParam("id", "coupon id", true)
@httpPatch("/:id/status", Guard.authSeller())
public async changeCouponStatus(@request() req: Request, @requestParam("id") couponId: string) {
const seller = req.user as ISeller;
const data = await this.couponService.changeCouponStatus(seller._id.toString(), couponId, OwnerRef.SELLER);
return this.response(data);
}
@ApiOperation("update a seller coupon ==> login as seller")
@ApiResponse("successful")
@ApiModel(UpdateCouponDTO)
@ApiParam("id", "coupon id", true)
@ApiAuth()
@httpPatch("/:id", Guard.authSeller(), ValidationMiddleware.validateInput(UpdateCouponDTO))
public async updateSellerCoupon(
@requestBody() updateDto: UpdateCouponDTO,
@request() req: Request,
@requestParam("id") couponId: string,
) {
const seller = req.user as ISeller;
const data = await this.couponService.updateCoupon(updateDto, seller._id.toString(), couponId, OwnerRef.SELLER);
return this.response(data);
}
@ApiOperation("add a coupon to user cart ===> login as user")
@ApiResponse("successful")
@ApiResponse("bad request", HttpStatus.BadRequest)
@ApiModel(ValidateCouponDTO)
@ApiAuth()
@httpPost("/add-coupon", Guard.authUser(), ValidationMiddleware.validateInput(ValidateCouponDTO))
public async addCoupon(@requestBody() validateDto: ValidateCouponDTO, @request() req: Request) {
const user = req.user as IUser;
const data = await this.couponService.applyCouponDiscount(validateDto, user._id.toString());
return this.response(data);
}
}
export { CouponController };
+24
View File
@@ -0,0 +1,24 @@
import { ICoupon } from "./models/Abstraction/ICoupon";
import { ICouponUsage } from "./models/Abstraction/ICouponUsage";
import { CouponModel } from "./models/coupon.model";
import { CouponUsageModel } from "./models/couponUsage.model";
import { BaseRepository } from "../../common/base/repository";
export class CouponRepo extends BaseRepository<ICoupon> {
constructor() {
super(CouponModel);
}
}
export function createCouponRepo(): CouponRepo {
return new CouponRepo();
}
export class CouponUsageRepo extends BaseRepository<ICouponUsage> {
constructor() {
super(CouponUsageModel);
}
}
export function createCouponUsageRepo(): CouponUsageRepo {
return new CouponUsageRepo();
}
+287
View File
@@ -0,0 +1,287 @@
import { randomBytes } from "crypto";
import { inject, injectable } from "inversify";
import { ClientSession, isValidObjectId, startSession } from "mongoose";
import { CouponRepo, CouponUsageRepo } from "./coupon.repository";
import { CreateCouponDTO } from "./DTO/createCoupon.dto";
import { UpdateCouponDTO } from "./DTO/updateCoupon.dto";
import { ValidateCouponDTO } from "./DTO/validateCoupon.dto";
import { CartMessage, CommonMessage, CouponMessage, ShopMessage } from "../../common/enums/message.enum";
import { BadRequestError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types";
import { TimeService } from "../../utils/time.service";
import { CartRepository, CartShipItemRepo } from "../cart/cart.repository";
import { CartService } from "../cart/cart.service";
import { ICartShipmentItem } from "../cart/models/Abstraction/ICartShipmentItem";
import { PaymentService } from "../payment/providers/payment.service";
import { OwnerRef } from "../shop/models/Abstraction/IShop";
import { ShopRepo } from "../shop/shop.repository";
@injectable()
class CouponService {
@inject(IOCTYPES.CouponRepo) couponRepo: CouponRepo;
@inject(IOCTYPES.CouponUsageRepo) couponUsageRepo: CouponUsageRepo;
@inject(IOCTYPES.PaymentService) paymentService: PaymentService;
@inject(IOCTYPES.CartRepository) cartRepo: CartRepository;
@inject(IOCTYPES.CartService) cartService: CartService;
@inject(IOCTYPES.ShopRepo) shopRepo: ShopRepo;
@inject(IOCTYPES.CartShipItemRepo) cartShipItemRepo: CartShipItemRepo;
async getSellersCoupons() {
const coupons = await this.couponRepo.model.aggregate([
{
$match: {
deleted: false,
},
},
{
$lookup: {
from: "shops",
localField: "shopId",
foreignField: "_id",
as: "shop",
},
},
{
$unwind: "$shop",
},
{
$match: {
"shop.ownerRef": OwnerRef.SELLER,
"shop.deleted": false,
},
},
{
$project: {
shop: 0,
},
},
]);
return { coupons };
}
async getSellerCoupons(ownerId: string, ownerRef: OwnerRef) {
const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
return {
coupons: await this.couponRepo.model.find({ shopId: shop._id, deleted: false }),
};
}
async getSingleCoupon(couponId: string) {
if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId);
const coupon = await this.couponRepo.model.findById(couponId);
if (!coupon) throw new BadRequestError(CouponMessage.InvalidId);
return { coupon };
}
//##############################
async createCoupon(createDto: CreateCouponDTO, ownerId: string, ownerRef: OwnerRef) {
const { discountAmount, discountPercentage, includedProducts, excludedProducts } = createDto;
if (!discountAmount && !discountPercentage) throw new BadRequestError(CouponMessage.DiscountAmountOrPercentageRequired);
if (discountAmount && discountPercentage) throw new BadRequestError(CouponMessage.BothDiscountOrPercentageShouldNotSend);
if (includedProducts && excludedProducts) throw new BadRequestError(CouponMessage.BothIncludedAndExcludedProductsShouldNotSend);
const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const code = this.generateCouponCode();
const coupon = await this.couponRepo.model.create({
shopId: shop._id,
code,
...createDto,
});
return { coupon };
}
//##############################
async updateCoupon(updateDto: UpdateCouponDTO, ownerId: string, couponId: string, ownerRef: OwnerRef) {
if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId);
const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const { discountAmount, discountPercentage, includedProducts, excludedProducts } = updateDto;
if (!discountAmount && !discountPercentage) throw new BadRequestError(CouponMessage.DiscountAmountOrPercentageRequired);
if (discountAmount && discountPercentage) throw new BadRequestError(CouponMessage.BothDiscountOrPercentageShouldNotSend);
if (includedProducts && excludedProducts) throw new BadRequestError(CouponMessage.BothIncludedAndExcludedProductsShouldNotSend);
const updatedCoupon = await this.couponRepo.model.findOneAndUpdate(
{ _id: couponId, shopId: shop._id },
{ ...updateDto },
{ new: true },
);
if (!updatedCoupon) throw new BadRequestError(CouponMessage.InvalidId);
return {
message: CommonMessage.Updated,
coupon: updatedCoupon,
};
}
//##############################
async changeCouponStatus(ownerId: string, couponId: string, ownerRef: OwnerRef) {
if (!isValidObjectId(couponId)) throw new BadRequestError(CouponMessage.InvalidId);
const shop = await this.shopRepo.model.findOne({ owner: ownerId, ownerRef });
if (!shop) throw new BadRequestError(ShopMessage.ShopNotFound);
const coupon = await this.couponRepo.model.findOne({ _id: couponId, shopId: shop._id });
if (!coupon) throw new BadRequestError(CouponMessage.InvalidId);
const updatedCoupon = await this.couponRepo.model.findOneAndUpdate(
{ _id: couponId, shopId: shop._id },
{ isActive: !coupon.isActive },
{ new: true },
);
return {
message: CommonMessage.Updated,
coupon: updatedCoupon,
};
}
//##############################
async applyCouponDiscount(validateDto: ValidateCouponDTO, userId: string) {
const session = await startSession();
session.startTransaction();
try {
const { price, cartShipmentItems } = await this.paymentService.getPaymentInfoS(userId);
const userCart = await this.cartRepo.model.findOne({ user: userId }).session(session);
if (!userCart) throw new BadRequestError(CartMessage.CartNotFound);
// if (cart.coupon_discount) throw new BadRequestError(CouponMessage.CouponAlreadyInCart);
const cartShipmentItem = cartShipmentItems.find((item) => item._id.toString() === validateDto.cartShipmentItemId);
if (!cartShipmentItem) throw new BadRequestError(CartMessage.InvalidCartShipmentItem);
const validCartShipmentItem = await this.cartShipItemRepo.model.findById(cartShipmentItem._id).session(session);
if (!validCartShipmentItem) throw new BadRequestError(CartMessage.InvalidCartShipmentItem);
if (cartShipmentItem.totalCouponDiscount) throw new BadRequestError(CouponMessage.CouponAlreadyInCart);
const { total_payable_price } = price;
const { coupon } = await this.validateCoupon(validateDto.code, cartShipmentItem.totalPaymentPrice, userId, session);
let discount = 0;
if (coupon.discountAmount) {
discount = coupon.discountAmount;
} else if (coupon.discountPercentage) {
discount = (total_payable_price * coupon.discountPercentage) / 100;
}
if (coupon.category && coupon.category.length > 0) {
await this.checkCartItemCategory(
userId,
coupon.category.map((cat) => cat._id.toString()),
);
}
if (coupon.includedProducts && coupon.includedProducts.length > 0) {
await this.checkCartItemIncludedProducts(cartShipmentItem, coupon.includedProducts);
}
if (coupon.excludedProducts && coupon.excludedProducts.length > 0) {
await this.checkCartItemExcludedProducts(cartShipmentItem, coupon.excludedProducts);
}
await this.couponUsageRepo.model.findOneAndUpdate(
{ user: userId, coupon: coupon._id },
{ $inc: { usageCount: 1 } },
{ new: true, upsert: true, session },
);
cartShipmentItem.totalCouponDiscount = discount;
cartShipmentItem.totalPaymentPrice = cartShipmentItem.totalPaymentPrice - discount;
userCart.coupon_discount = discount;
coupon.usageCount += 1;
await validCartShipmentItem.save({ session });
await userCart.save({ session });
await coupon.save({ session });
await session.commitTransaction();
await session.endSession();
return { message: CouponMessage.CouponAdded, afterDiscount: total_payable_price - discount };
} catch (error) {
await session.abortTransaction();
await session.endSession();
throw error;
}
}
//================================> helper method
private generateCouponCode(length = 8) {
return randomBytes(length).toString("hex").slice(0, length).toUpperCase();
}
async checkCartItemExcludedProducts(shipmentItem: ICartShipmentItem, excludedProducts: number[]) {
const items = shipmentItem.shipmentItems.filter((item) => excludedProducts.includes(item.product));
console.log({ items });
if (items.length > 0) throw new BadRequestError(CouponMessage.InvalidProduct);
return true;
}
async checkCartItemIncludedProducts(shipmentItem: ICartShipmentItem, includedProducts: number[]) {
const items = shipmentItem.shipmentItems.filter((item) => includedProducts.includes(item.product));
if (!items.length) throw new BadRequestError(CouponMessage.InvalidProduct);
return true;
}
async checkCartItemCategory(userId: string, category: string[]) {
const { cart } = await this.cartService.getCartS(userId);
const items = cart.items.filter((item) => category.includes(item.product.category._id));
if (items.length === 0) throw new BadRequestError(CouponMessage.InvalidCategory);
return true;
}
async removeCouponByAdmin(couponId: string) {
const deletedCoupon = await this.couponRepo.model.findByIdAndUpdate(couponId, { deleted: true }, { new: true });
if (!deletedCoupon) throw new BadRequestError(CouponMessage.NotFound);
return { message: CommonMessage.Deleted };
}
//##############################
private async validateCoupon(couponCode: string, totalPrice: number, userId: string, session: ClientSession) {
const coupon = await this.couponRepo.model.findOne({ code: couponCode, isActive: true, deleted: false }).session(session);
if (!coupon) throw new BadRequestError(CouponMessage.NotFound);
const expired = TimeService.isPersianDateExpired(coupon.expirationDate);
if (expired) throw new BadRequestError(CouponMessage.Expired);
if (coupon.usageLimit && coupon.usageCount >= coupon.usageLimit) throw new BadRequestError(CouponMessage.UsageLimit);
const userUsage = await this.couponUsageRepo.model.findOne({ coupon: coupon._id, user: userId }).session(session);
if (userUsage && userUsage.usageCount >= coupon.userUsageLimit) throw new BadRequestError(CouponMessage.UserCouponUsageLimit);
if (coupon.minPurchaseAmount && totalPrice < coupon.minPurchaseAmount) {
throw new BadRequestError([
CouponMessage.AmountIsNotSatisfied,
`حداقل مبلغ سبد خرید برای استفاده از این کد تخفیف ${coupon.minPurchaseAmount}`,
]);
}
return { coupon };
}
}
export { CouponService };
@@ -0,0 +1,20 @@
import { Types } from "mongoose";
export interface ICoupon {
shopId: Types.ObjectId;
discountAmount: number;
discountPercentage: number;
usageLimit: number;
usageCount: number;
userUsageLimit: number;
code: string;
minPurchaseAmount: number;
category: Types.ObjectId[];
includedProducts: number[];
excludedProducts: number[];
description: string;
isActive: boolean;
includeSpecialSale: boolean;
expirationDate: string;
deleted: boolean;
}
@@ -0,0 +1,7 @@
import { Types } from "mongoose";
export interface ICouponUsage {
coupon: Types.ObjectId;
user: Types.ObjectId;
usageCount: number;
}
+29
View File
@@ -0,0 +1,29 @@
import { Schema, model } from "mongoose";
import { ICoupon } from "./Abstraction/ICoupon";
const CouponSchema = new Schema<ICoupon>(
{
shopId: { type: Schema.Types.ObjectId, ref: "Shop", required: true },
discountPercentage: { type: Number, default: 0 },
discountAmount: { type: Number, default: 0 },
usageLimit: { type: Number, default: null },
usageCount: { type: Number, default: 0 },
userUsageLimit: { type: Number, default: null },
code: { type: String, required: true, unique: true },
minPurchaseAmount: { type: Number, default: null },
category: { type: [Schema.Types.ObjectId], ref: "Category", default: [] },
includedProducts: { type: [Number], ref: "Product", default: [] },
excludedProducts: { type: [Number], ref: "Product", default: [] },
description: { type: String, default: null },
isActive: { type: Boolean, default: true },
includeSpecialSale: { type: Boolean, default: true },
expirationDate: { type: String, required: true },
deleted: { type: Boolean, default: false },
},
{ timestamps: true, toJSON: { versionKey: false }, id: false },
);
const CouponModel = model<ICoupon>("Coupon", CouponSchema);
export { CouponModel };
@@ -0,0 +1,16 @@
import { Schema, model } from "mongoose";
import { ICouponUsage } from "./Abstraction/ICouponUsage";
const CouponUsageSchema = new Schema<ICouponUsage>(
{
coupon: { type: Schema.Types.ObjectId, ref: "Coupon", required: true },
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
usageCount: { type: Number, required: true },
},
{ timestamps: true, toJSON: { versionKey: false }, id: false },
);
CouponUsageSchema.index({ coupon: 1, user: 1 }, { unique: true });
const CouponUsageModel = model<ICouponUsage>("CouponUsage", CouponUsageSchema);
export { CouponUsageModel };
+52
View File
@@ -0,0 +1,52 @@
import { inject, injectable } from "inversify";
import { AsanPardakhtGateway } from "./gateways/asanpardakht";
import { ZarinPalGateway } from "./gateways/zarinpal";
import { IPaymentGateway, NewPaymentData, VerifyPaymentData } from "./interface/IPG";
import { GatewayProvider } from "../../common/enums/payment.enum";
import { BadRequestError } from "../../core/app/app.errors";
import { IOCTYPES } from "../../IOC/ioc.types";
@injectable()
class PaymentGateway implements IPaymentGateway {
@inject(IOCTYPES.ZarinPalGateway) zarinPalGateway: ZarinPalGateway;
@inject(IOCTYPES.AsanPardakhtGateway) asanpardakhtGateway: AsanPardakhtGateway;
public async requestPayment(gatewayType: GatewayProvider, data: NewPaymentData): Promise<any> {
switch (gatewayType) {
case GatewayProvider.Zarinpal:
return await this.zarinPalGateway.processPayment(data.amount, data.description, data.email, data.mobile, data.callbackPath);
case GatewayProvider.Asanpardakht:
//TODO:fix this
return this.asanpardakhtGateway.token();
default:
throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
}
}
public async verifyPayment(gatewayType: GatewayProvider, data: VerifyPaymentData): Promise<any> {
switch (gatewayType) {
case GatewayProvider.Zarinpal:
return await this.zarinPalGateway.verifyPayment(data.authority, data.amount);
case GatewayProvider.Asanpardakht:
//TODO:fix this
return this.asanpardakhtGateway.time();
default:
throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
}
}
public async retryPayment(gatewayType: GatewayProvider, authority: string) {
switch (gatewayType) {
case GatewayProvider.Zarinpal:
return await this.zarinPalGateway.retryPayment(authority);
case GatewayProvider.Asanpardakht:
//TODO:fix this
return this.asanpardakhtGateway.token();
default:
throw new BadRequestError(`Payment provider ${gatewayType} is not supported`);
}
}
}
export { PaymentGateway };
+141
View File
@@ -0,0 +1,141 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { injectable } from "inversify";
@injectable()
class AsanPardakhtGateway {
private invoiceId: string | null = null;
private amount: number | null = null;
private callBackURL = `${process.env.SITE_URL}/verify/ap`;
private readonly GatewayAuth = {
user: process.env.ASANPARDAKHT_USER,
password: process.env.ASANPARDAKHT_PASS,
merchant_id: process.env.ASANPARDAKHT_MERCHANT_ID,
merchantConfigID: process.env.ASANPARDAKHT_MERCHANT_CONFIG_ID,
};
// API endpoint URLs
private readonly TokenURL = "v1/Token";
private readonly TimeURL = "v1/Time";
private readonly TranResultURL = "v1/TranResult";
private readonly CardHashURL = "v1/CardHash";
private readonly SettlementURL = "v1/Settlement";
private readonly VerifyURL = "v1/Verify";
private readonly CancelURL = "v1/Cancel";
private readonly ReverseURL = "v1/Reverse";
private readonly GatewayApiURL = "https://ipgrest.asanpardakht.ir";
setInvoiceId(id: string): this {
this.invoiceId = id;
return this;
}
setAmount(amount: number): this {
this.amount = amount;
return this;
}
async time(): Promise<{ code: number; content: any }> {
return await this.callAPI("GET", this.TimeURL);
}
async TranResult(): Promise<{ code: number; content: any }> {
const res = await this.callAPI(
"GET",
`${this.TranResultURL}?${new URLSearchParams({
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
localInvoiceId: this.invoiceId!,
}).toString()}`,
);
return { code: res.code, content: JSON.parse(res.content) };
}
async CardHash(): Promise<{ code: number; content: any }> {
return await this.callAPI(
"GET",
`${this.CardHashURL}?${new URLSearchParams({
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
localInvoiceId: this.invoiceId!,
}).toString()}`,
);
}
async token(): Promise<{ code: number; content: any }> {
if (this.callBackURL) {
this.callBackURL += this.callBackURL.includes("?") ? "&" : "?";
}
const currentDate = new Date().toISOString().split("T").join(" ").split(".")[0].replace(/[-:]/g, "");
return await this.callAPI("POST", this.TokenURL, {
serviceTypeId: 1,
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
localInvoiceId: this.invoiceId!,
amountInRials: this.amount!,
localDate: currentDate,
callbackURL: `${this.callBackURL}${new URLSearchParams({ invoice: this.invoiceId! }).toString()}`,
paymentId: 0,
additionalData: "",
});
}
async settlement(transId: string): Promise<{ code: number; content: any }> {
return await this.callAPI("POST", this.SettlementURL, {
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
payGateTranId: transId,
});
}
async verify(transId: string): Promise<{ code: number; content: any }> {
return await this.callAPI("POST", this.VerifyURL, {
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
payGateTranId: transId,
});
}
async reverse(transId: string): Promise<{ code: number; content: any }> {
return await this.callAPI("POST", this.ReverseURL, {
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
payGateTranId: transId,
});
}
async cancel(transId: string): Promise<{ code: number; content: any }> {
return await this.callAPI("POST", this.CancelURL, {
merchantConfigurationId: this.GatewayAuth.merchantConfigID!,
payGateTranId: transId,
});
}
/**
* Internal method to call the API.
* @param method - The HTTP method to use.
* @param endpoint - The API endpoint to call.
* @param data - Optional data to send with the request.
* @returns A promise resolving to the API response.
*/
private async callAPI(method: "GET" | "POST", endpoint: string, data: any = null): Promise<{ code: number; content: any }> {
const url = `${this.GatewayApiURL}/${endpoint}`;
const options: AxiosRequestConfig = {
method,
url,
headers: {
Accept: "application/json",
Usr: this.GatewayAuth.user || "",
Pwd: this.GatewayAuth.password || "",
"Content-Type": "application/json",
},
data: data ? JSON.stringify(data) : null,
};
try {
const response: AxiosResponse = await axios(options);
return { content: response.data, code: response.status };
} catch (error: any) {
if (error.response) {
return { content: error.response.data, code: error.response.status };
} else {
return { content: error.message, code: 500 };
}
}
}
}
export { AsanPardakhtGateway };
+64
View File
@@ -0,0 +1,64 @@
import axios from "axios";
import { injectable } from "inversify";
import { ZarinPalPGNewArgs, ZarinPalPGNewRequestData, ZarinPalPGVerifyData } from "../../../common/types/paymentGateway.type";
import { InternalError } from "../../../core/app/app.errors";
import { Logger } from "../../../core/logging/logger";
@injectable()
class ZarinPalGateway {
private logger = new Logger(ZarinPalGateway.name);
private gatewayApiUrl: string = `https://${process.env.IPG_TYPE}.zarinpal.com/pg`; //or api subdomain
private callBackURL = `${process.env.SITE_URL}/verify/Zarinpal/`;
private MERCHANT_ID: string = process.env.ZARINPAL_MERCHANT_ID;
private requestHeader = { "Content-Type": "application/json", Accept: "application/json" };
async processPayment(amount: number, description: string, email: string, mobile: string, callBackPath: string) {
try {
const purchaseData: ZarinPalPGNewArgs = {
merchant_id: this.MERCHANT_ID,
amount,
callback_url: `${this.callBackURL}${callBackPath}`,
description,
currency: "IRT" as const,
metadata: { email, mobile },
};
const response = await axios.post(`${this.gatewayApiUrl}/v4/payment/request.json`, purchaseData, { headers: this.requestHeader });
const { data } = response.data as ZarinPalPGNewRequestData;
return {
redirectUrl: `${this.gatewayApiUrl}/StartPay/${data.authority}`,
message: data.message,
authority: data.authority,
};
} catch (error) {
this.logger.error("error in payment process", error);
throw new InternalError(["error processing payment"]);
}
}
async verifyPayment(authority: string, amount: number) {
try {
//TODO:check if amount should be in toman or riyal
const verifyData = { authority, amount, merchant_id: this.MERCHANT_ID };
const response = await axios.post(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader });
const { data } = response.data as ZarinPalPGVerifyData;
return data;
} catch (error) {
this.logger.error("error in payment verification", error);
throw new InternalError(["error in payment verification"]);
}
}
async retryPayment(authority: string) {
return {
redirectUrl: `${this.gatewayApiUrl}/StartPay/${authority}`,
authority: authority,
};
}
}
export { ZarinPalGateway };
+37
View File
@@ -0,0 +1,37 @@
import { GatewayProvider } from "../../../common/enums/payment.enum";
export interface IPaymentGateway {
/**
*
* @param gatewayType
* @param data
*/
requestPayment(gatewayType: GatewayProvider, data: NewPaymentData): Promise<any>;
/**
*
* @param gatewayType
* @param data
*/
verifyPayment(gatewayType: GatewayProvider, data: VerifyPaymentData): Promise<any>;
/**
*
* @param gatewayType
* @param authority
*/
retryPayment(gatewayType: GatewayProvider, authority: string): Promise<any>;
}
export interface NewPaymentData {
amount: number;
description: string;
email: string;
mobile: string;
callbackPath: string;
}
export interface VerifyPaymentData {
authority: string;
amount: number;
}
@@ -0,0 +1,34 @@
import { Expose, plainToClass } from "class-transformer";
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IAboutUs } from "../model/Abstractions/IAboutUs";
export class CreateAboutUsDTO {
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "title of about us", example: "عنوان ۱" })
title: string;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "description of about us", example: "توضیحات درباره ما" })
description: string;
@Expose()
@IsNotEmpty()
@IsString()
@ApiProperty({ type: "string", description: "image url of about us", example: "https://loremflickr.com/454/340" })
imageUrl: string;
public static transformAboutUs(data: IAboutUs): CreateAboutUsDTO {
const AboutUsDto = plainToClass(CreateAboutUsDTO, data, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
return AboutUsDto;
}
}

Some files were not shown because too many files have changed in this diff Show More