commit 14e49374ac34cce0fb93d3d83c206e16bb167dc0 Author: Mars <3361409208a@gmail.com> Date: Tue Jun 2 14:35:48 2026 +0800 初始化足球投注平台 MVP Monorepo 包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ca997e4 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +DATABASE_URL=postgresql://thebet365:thebet365@localhost:5432/thebet365 +REDIS_URL=redis://localhost:6379 +JWT_SECRET=change-me-in-production-use-long-random-string +JWT_PLAYER_EXPIRES=24h +JWT_ADMIN_EXPIRES=2h +JWT_AGENT_EXPIRES=8h +PORT=3000 +NODE_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e64ddc --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.env +.env.local +*.log +.DS_Store +coverage/ +.turbo/ +*.tsbuildinfo +apps/api/prisma/migrations/*_migration_lock.toml diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9711d06 --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +only-built-dependencies[]=@prisma/client +only-built-dependencies[]=@prisma/engines +only-built-dependencies[]=prisma +only-built-dependencies[]=@nestjs/core +only-built-dependencies[]=esbuild +only-built-dependencies[]=vue-demi diff --git a/README.md b/README.md new file mode 100644 index 0000000..5262e12 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# TheBet365 - 足球投注平台 MVP + +Monorepo 项目,包含 NestJS 后端与 Vue 3 三端前端。 + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 后端 | NestJS + Prisma + PostgreSQL + Redis | +| 玩家前台 | Vue 3 + Vite + Pinia (H5 移动端优先) | +| 平台/代理后台 | Vue 3 + Vite + Element Plus | +| 包管理 | pnpm workspace | + +## 快速开始 + +### 1. 启动基础设施 + +```bash +docker compose up -d +``` + +### 2. 安装依赖 + +```bash +pnpm install +``` + +### 3. 配置环境 + +```bash +cp .env.example apps/api/.env +``` + +### 4. 数据库迁移与种子数据 + +```bash +pnpm db:migrate +pnpm db:seed +``` + +### 5. 启动开发服务 + +```bash +pnpm dev:api # API http://localhost:3000 +pnpm dev:player # 玩家前台 http://localhost:5173 +pnpm dev:admin # 平台后台 http://localhost:5174 +pnpm dev:agent # 代理后台 http://localhost:5175 +``` + +API 文档:http://localhost:3000/api/docs + +## 测试账号 + +| 角色 | 用户名 | 密码 | +|------|--------|------| +| 超级管理员 | admin | Admin@123 | +| 一级代理 | agent1 | Agent@123 | +| 二级代理 | agent2 | Agent@123 | +| 玩家 | player1 | Player@123 | + +## 项目结构 + +``` +apps/ + api/ NestJS 单体后端 + player/ 玩家 H5 前台 + admin/ 平台后台 + agent/ 代理后台 +packages/ + shared/ 共享类型与常量 +``` + +## MVP 功能 + +- 三端登录鉴权 + RBAC +- 2 级代理 + 信用额度池 +- 钱包账变(禁止直接改余额) +- 11 种足球玩法 + 盘口模板 +- 单关 / 2~5 串 1 下注 +- 结算预览 + 确认入账 +- 返水批次 +- Banner / 公告 / 多语言 + +## 测试 + +```bash +pnpm --filter @thebet365/api test +``` diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..334d0fd --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,12 @@ + + + + + + TheBet365 Admin + + +
+ + + diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..7b68c1c --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@thebet365/admin", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 5174", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "element-plus": "^2.9.3", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vue-tsc": "^2.2.0" + } +} diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue new file mode 100644 index 0000000..e571fd1 --- /dev/null +++ b/apps/admin/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/apps/admin/src/api.ts b/apps/admin/src/api.ts new file mode 100644 index 0000000..52f05ca --- /dev/null +++ b/apps/admin/src/api.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +const api = axios.create({ baseURL: '/api' }); +api.interceptors.request.use((c) => { + const t = localStorage.getItem('admin_token'); + if (t) c.headers.Authorization = `Bearer ${t}`; + return c; +}); +export default api; diff --git a/apps/admin/src/layouts/AdminLayout.vue b/apps/admin/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..1ef01b5 --- /dev/null +++ b/apps/admin/src/layouts/AdminLayout.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/admin/src/main.ts b/apps/admin/src/main.ts new file mode 100644 index 0000000..51855d3 --- /dev/null +++ b/apps/admin/src/main.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import App from './App.vue'; +import router from './router'; + +createApp(App).use(router).use(ElementPlus).mount('#app'); diff --git a/apps/admin/src/router/index.ts b/apps/admin/src/router/index.ts new file mode 100644 index 0000000..b460600 --- /dev/null +++ b/apps/admin/src/router/index.ts @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +export default createRouter({ + history: createWebHistory(), + routes: [ + { path: '/login', component: () => import('./views/Login.vue') }, + { + path: '/', + component: () => import('./layouts/AdminLayout.vue'), + meta: { auth: true }, + children: [ + { path: '', component: () => import('./views/Dashboard.vue') }, + { path: 'users', component: () => import('./views/Users.vue') }, + { path: 'agents', component: () => import('./views/Agents.vue') }, + { path: 'matches', component: () => import('./views/Matches.vue') }, + { path: 'bets', component: () => import('./views/Bets.vue') }, + { path: 'settlement/:id', component: () => import('./views/Settlement.vue') }, + { path: 'cashback', component: () => import('./views/Cashback.vue') }, + { path: 'audit', component: () => import('./views/Audit.vue') }, + ], + }, + ], +}); diff --git a/apps/admin/src/views/Agents.vue b/apps/admin/src/views/Agents.vue new file mode 100644 index 0000000..9131df1 --- /dev/null +++ b/apps/admin/src/views/Agents.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/admin/src/views/Audit.vue b/apps/admin/src/views/Audit.vue new file mode 100644 index 0000000..f738b7c --- /dev/null +++ b/apps/admin/src/views/Audit.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/admin/src/views/Bets.vue b/apps/admin/src/views/Bets.vue new file mode 100644 index 0000000..6997ac8 --- /dev/null +++ b/apps/admin/src/views/Bets.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/admin/src/views/Cashback.vue b/apps/admin/src/views/Cashback.vue new file mode 100644 index 0000000..1a8f5e3 --- /dev/null +++ b/apps/admin/src/views/Cashback.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/admin/src/views/Dashboard.vue b/apps/admin/src/views/Dashboard.vue new file mode 100644 index 0000000..481ff89 --- /dev/null +++ b/apps/admin/src/views/Dashboard.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/admin/src/views/Login.vue b/apps/admin/src/views/Login.vue new file mode 100644 index 0000000..c9b848e --- /dev/null +++ b/apps/admin/src/views/Login.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/admin/src/views/Matches.vue b/apps/admin/src/views/Matches.vue new file mode 100644 index 0000000..fd74902 --- /dev/null +++ b/apps/admin/src/views/Matches.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/admin/src/views/Settlement.vue b/apps/admin/src/views/Settlement.vue new file mode 100644 index 0000000..43092c3 --- /dev/null +++ b/apps/admin/src/views/Settlement.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/admin/src/views/Users.vue b/apps/admin/src/views/Users.vue new file mode 100644 index 0000000..fde65bb --- /dev/null +++ b/apps/admin/src/views/Users.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/admin/src/vite-env.d.ts b/apps/admin/src/vite-env.d.ts new file mode 100644 index 0000000..aad3656 --- /dev/null +++ b/apps/admin/src/vite-env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent; + export default component; +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..be1c505 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true }, "include": ["src/**/*.ts", "src/**/*.vue"] } diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..ef88671 --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5174, + proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } }, + }, +}); diff --git a/apps/agent/index.html b/apps/agent/index.html new file mode 100644 index 0000000..7447517 --- /dev/null +++ b/apps/agent/index.html @@ -0,0 +1,5 @@ + + +TheBet365 Agent +
+ diff --git a/apps/agent/package.json b/apps/agent/package.json new file mode 100644 index 0000000..60598aa --- /dev/null +++ b/apps/agent/package.json @@ -0,0 +1,23 @@ +{ + "name": "@thebet365/agent", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 5175", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.9", + "element-plus": "^2.9.3", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vue-tsc": "^2.2.0" + } +} diff --git a/apps/agent/src/App.vue b/apps/agent/src/App.vue new file mode 100644 index 0000000..6f2288a --- /dev/null +++ b/apps/agent/src/App.vue @@ -0,0 +1 @@ + diff --git a/apps/agent/src/api.ts b/apps/agent/src/api.ts new file mode 100644 index 0000000..090e67f --- /dev/null +++ b/apps/agent/src/api.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +const api = axios.create({ baseURL: '/api' }); +api.interceptors.request.use((c) => { + const t = localStorage.getItem('agent_token'); + if (t) c.headers.Authorization = `Bearer ${t}`; + return c; +}); +export default api; diff --git a/apps/agent/src/layouts/AgentLayout.vue b/apps/agent/src/layouts/AgentLayout.vue new file mode 100644 index 0000000..04bca9f --- /dev/null +++ b/apps/agent/src/layouts/AgentLayout.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/agent/src/main.ts b/apps/agent/src/main.ts new file mode 100644 index 0000000..51855d3 --- /dev/null +++ b/apps/agent/src/main.ts @@ -0,0 +1,7 @@ +import { createApp } from 'vue'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import App from './App.vue'; +import router from './router'; + +createApp(App).use(router).use(ElementPlus).mount('#app'); diff --git a/apps/agent/src/router/index.ts b/apps/agent/src/router/index.ts new file mode 100644 index 0000000..94d8eae --- /dev/null +++ b/apps/agent/src/router/index.ts @@ -0,0 +1,18 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +export default createRouter({ + history: createWebHistory(), + routes: [ + { path: '/login', component: () => import('./views/Login.vue') }, + { + path: '/', + component: () => import('./layouts/AgentLayout.vue'), + children: [ + { path: '', component: () => import('./views/Dashboard.vue') }, + { path: 'players', component: () => import('./views/Players.vue') }, + { path: 'agents', component: () => import('./views/SubAgents.vue') }, + { path: 'bets', component: () => import('./views/Bets.vue') }, + ], + }, + ], +}); diff --git a/apps/agent/src/views/Bets.vue b/apps/agent/src/views/Bets.vue new file mode 100644 index 0000000..d4d4bd2 --- /dev/null +++ b/apps/agent/src/views/Bets.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/agent/src/views/Dashboard.vue b/apps/agent/src/views/Dashboard.vue new file mode 100644 index 0000000..e2c25fc --- /dev/null +++ b/apps/agent/src/views/Dashboard.vue @@ -0,0 +1,21 @@ + + + diff --git a/apps/agent/src/views/Login.vue b/apps/agent/src/views/Login.vue new file mode 100644 index 0000000..4656b2b --- /dev/null +++ b/apps/agent/src/views/Login.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/agent/src/views/Players.vue b/apps/agent/src/views/Players.vue new file mode 100644 index 0000000..fee4721 --- /dev/null +++ b/apps/agent/src/views/Players.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/agent/src/views/SubAgents.vue b/apps/agent/src/views/SubAgents.vue new file mode 100644 index 0000000..738c319 --- /dev/null +++ b/apps/agent/src/views/SubAgents.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/agent/src/vite-env.d.ts b/apps/agent/src/vite-env.d.ts new file mode 100644 index 0000000..aad3656 --- /dev/null +++ b/apps/agent/src/vite-env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent; + export default component; +} diff --git a/apps/agent/tsconfig.json b/apps/agent/tsconfig.json new file mode 100644 index 0000000..be1c505 --- /dev/null +++ b/apps/agent/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true }, "include": ["src/**/*.ts", "src/**/*.vue"] } diff --git a/apps/agent/vite.config.ts b/apps/agent/vite.config.ts new file mode 100644 index 0000000..b570aba --- /dev/null +++ b/apps/agent/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5175, + proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } }, + }, +}); diff --git a/apps/api/jest.config.js b/apps/api/jest.config.js new file mode 100644 index 0000000..870177f --- /dev/null +++ b/apps/api/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: 'src', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + moduleNameMapper: { + '^@/(.*)$': '/$1', + }, +}; diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/api/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..6ef20c4 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,59 @@ +{ + "name": "@thebet365/api", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "start": "node dist/main", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:migrate:deploy": "prisma migrate deploy", + "db:seed": "ts-node prisma/seed.ts", + "db:studio": "prisma studio" + }, + "dependencies": { + "@nestjs/common": "^11.0.6", + "@nestjs/config": "^4.0.0", + "@nestjs/core": "^11.0.6", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.6", + "@nestjs/schedule": "^5.0.1", + "@nestjs/swagger": "^11.0.3", + "@prisma/client": "^6.3.1", + "@thebet365/shared": "workspace:*", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "decimal.js": "^10.4.3", + "ioredis": "^5.4.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.2", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.6", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", + "@types/uuid": "^10.0.0", + "jest": "^29.7.0", + "prisma": "^6.3.1", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + } +} diff --git a/apps/api/prisma/migrations/README.md b/apps/api/prisma/migrations/README.md new file mode 100644 index 0000000..0502fa4 --- /dev/null +++ b/apps/api/prisma/migrations/README.md @@ -0,0 +1,5 @@ +-- Initial migration for TheBet365 MVP +-- Run: pnpm --filter @thebet365/api db:migrate + +-- Generated from prisma/schema.prisma +-- Use `pnpm db:migrate` when PostgreSQL is available diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..3f00286 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -0,0 +1,585 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ============ Users & Auth ============ + +model User { + id BigInt @id @default(autoincrement()) + username String @unique @db.VarChar(64) + userType String @map("user_type") @db.VarChar(20) + status String @default("ACTIVE") @db.VarChar(20) + parentId BigInt? @map("parent_id") + agentLevel Int? @map("agent_level") + locale String @default("en-US") @db.VarChar(10) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + auth UserAuth? + wallet Wallet? + agentProfile AgentProfile? + adminRole AdminUserRole? + bets Bet[] + preferences UserPreference? + + parent User? @relation("UserHierarchy", fields: [parentId], references: [id]) + children User[] @relation("UserHierarchy") + + @@index([userType]) + @@index([parentId]) + @@map("users") +} + +model UserAuth { + id BigInt @id @default(autoincrement()) + userId BigInt @unique @map("user_id") + passwordHash String @map("password_hash") @db.VarChar(255) + loginFailCount Int @default(0) @map("login_fail_count") + lockedUntil DateTime? @map("locked_until") + lastLoginAt DateTime? @map("last_login_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@map("user_auth") +} + +model UserPreference { + id BigInt @id @default(autoincrement()) + userId BigInt @unique @map("user_id") + locale String @default("en-US") @db.VarChar(10) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@map("user_preferences") +} + +model Role { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(64) + name String @db.VarChar(128) + description String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + + permissions RolePermission[] + adminUsers AdminUserRole[] + + @@map("roles") +} + +model Permission { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(128) + name String @db.VarChar(128) + module String @db.VarChar(64) + createdAt DateTime @default(now()) @map("created_at") + + roles RolePermission[] + + @@map("permissions") +} + +model RolePermission { + roleId BigInt @map("role_id") + permissionId BigInt @map("permission_id") + + role Role @relation(fields: [roleId], references: [id]) + permission Permission @relation(fields: [permissionId], references: [id]) + + @@id([roleId, permissionId]) + @@map("role_permissions") +} + +model AdminUserRole { + userId BigInt @unique @map("user_id") + roleId BigInt @map("role_id") + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) + + @@map("admin_user_roles") +} + +// ============ Agent ============ + +model AgentProfile { + id BigInt @id @default(autoincrement()) + userId BigInt @unique @map("user_id") + level Int + parentAgentId BigInt? @map("parent_agent_id") + creditLimit Decimal @default(0) @map("credit_limit") @db.Decimal(18, 4) + usedCredit Decimal @default(0) @map("used_credit") @db.Decimal(18, 4) + directPlayerLiability Decimal @default(0) @map("direct_player_liability") @db.Decimal(18, 4) + childAgentExposure Decimal @default(0) @map("child_agent_exposure") @db.Decimal(18, 4) + status String @default("ACTIVE") @db.VarChar(20) + maxSingleDeposit Decimal? @map("max_single_deposit") @db.Decimal(18, 4) + maxDailyDeposit Decimal? @map("max_daily_deposit") @db.Decimal(18, 4) + cashbackRate Decimal @default(0) @map("cashback_rate") @db.Decimal(8, 4) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + + @@index([parentAgentId]) + @@map("agent_profiles") +} + +model AgentClosure { + ancestorId BigInt @map("ancestor_id") + descendantId BigInt @map("descendant_id") + depth Int + + @@id([ancestorId, descendantId]) + @@index([descendantId]) + @@map("agent_closure") +} + +model AgentCreditTransaction { + id BigInt @id @default(autoincrement()) + agentId BigInt @map("agent_id") + transactionType String @map("transaction_type") @db.VarChar(32) + amount Decimal @db.Decimal(18, 4) + creditBefore Decimal @map("credit_before") @db.Decimal(18, 4) + creditAfter Decimal @map("credit_after") @db.Decimal(18, 4) + referenceType String? @map("reference_type") @db.VarChar(32) + referenceId String? @map("reference_id") @db.VarChar(64) + operatorId BigInt? @map("operator_id") + requestId String? @map("request_id") @db.VarChar(128) + remark String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + @@unique([operatorId, requestId]) + @@index([agentId]) + @@map("agent_credit_transactions") +} + +// ============ Wallet ============ + +model Wallet { + id BigInt @id @default(autoincrement()) + userId BigInt @unique @map("user_id") + availableBalance Decimal @default(0) @map("available_balance") @db.Decimal(18, 4) + frozenBalance Decimal @default(0) @map("frozen_balance") @db.Decimal(18, 4) + currency String @default("USD") @db.VarChar(16) + status String @default("ACTIVE") @db.VarChar(20) + version Int @default(0) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + transactions WalletTransaction[] + + @@map("wallets") +} + +model WalletTransaction { + id BigInt @id @default(autoincrement()) + transactionId String @unique @map("transaction_id") @db.VarChar(64) + userId BigInt @map("user_id") + walletId BigInt @map("wallet_id") + transactionType String @map("transaction_type") @db.VarChar(32) + amount Decimal @db.Decimal(18, 4) + balanceBefore Decimal @map("balance_before") @db.Decimal(18, 4) + balanceAfter Decimal @map("balance_after") @db.Decimal(18, 4) + frozenBefore Decimal @map("frozen_before") @db.Decimal(18, 4) + frozenAfter Decimal @map("frozen_after") @db.Decimal(18, 4) + referenceType String? @map("reference_type") @db.VarChar(32) + referenceId String? @map("reference_id") @db.VarChar(64) + operatorId BigInt? @map("operator_id") + remark String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + wallet Wallet @relation(fields: [walletId], references: [id]) + + @@index([userId]) + @@index([walletId]) + @@index([createdAt]) + @@map("wallet_transactions") +} + +// ============ Sports Data ============ + +model League { + id BigInt @id @default(autoincrement()) + sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20) + code String @unique @db.VarChar(64) + displayOrder Int @default(0) @map("display_order") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + matches Match[] + + @@map("leagues") +} + +model Team { + id BigInt @id @default(autoincrement()) + sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20) + code String @unique @db.VarChar(64) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + homeMatches Match[] @relation("HomeTeam") + awayMatches Match[] @relation("AwayTeam") + + @@map("teams") +} + +model EntityTranslation { + id BigInt @id @default(autoincrement()) + entityType String @map("entity_type") @db.VarChar(32) + entityId BigInt @map("entity_id") + locale String @db.VarChar(10) + fieldName String @map("field_name") @db.VarChar(32) + value String @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([entityType, entityId, locale, fieldName]) + @@index([entityType, entityId]) + @@map("entity_translations") +} + +model Match { + id BigInt @id @default(autoincrement()) + sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20) + leagueId BigInt @map("league_id") + homeTeamId BigInt @map("home_team_id") + awayTeamId BigInt @map("away_team_id") + startTime DateTime @map("start_time") + status String @default("DRAFT") @db.VarChar(32) + isHot Boolean @default(false) @map("is_hot") + displayOrder Int @default(0) @map("display_order") + publishTime DateTime? @map("publish_time") + closeTime DateTime? @map("close_time") + isOutright Boolean @default(false) @map("is_outright") + createdBy BigInt? @map("created_by") + updatedBy BigInt? @map("updated_by") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + league League @relation(fields: [leagueId], references: [id]) + homeTeam Team @relation("HomeTeam", fields: [homeTeamId], references: [id]) + awayTeam Team @relation("AwayTeam", fields: [awayTeamId], references: [id]) + score MatchScore? + markets Market[] + settlements SettlementBatch[] + + @@index([status]) + @@index([startTime]) + @@index([leagueId]) + @@map("matches") +} + +model MatchScore { + id BigInt @id @default(autoincrement()) + matchId BigInt @unique @map("match_id") + htHomeScore Int? @map("ht_home_score") + htAwayScore Int? @map("ht_away_score") + ftHomeScore Int? @map("ft_home_score") + ftAwayScore Int? @map("ft_away_score") + winnerTeamId BigInt? @map("winner_team_id") + recordedBy BigInt? @map("recorded_by") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + match Match @relation(fields: [matchId], references: [id]) + + @@map("match_scores") +} + +model Market { + id BigInt @id @default(autoincrement()) + matchId BigInt @map("match_id") + marketType String @map("market_type") @db.VarChar(64) + period String @db.VarChar(16) + lineValue Decimal? @map("line_value") @db.Decimal(8, 2) + status String @default("OPEN") @db.VarChar(20) + allowSingle Boolean @default(true) @map("allow_single") + allowParlay Boolean @default(true) @map("allow_parlay") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + match Match @relation(fields: [matchId], references: [id]) + selections MarketSelection[] + + @@index([matchId]) + @@index([marketType]) + @@map("markets") +} + +model MarketSelection { + id BigInt @id @default(autoincrement()) + marketId BigInt @map("market_id") + selectionCode String @map("selection_code") @db.VarChar(64) + selectionName String @map("selection_name") @db.VarChar(255) + odds Decimal @db.Decimal(18, 6) + oddsVersion BigInt @default(1) @map("odds_version") + status String @default("OPEN") @db.VarChar(20) + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + market Market @relation(fields: [marketId], references: [id]) + oddsLogs OddsChangeLog[] + + @@index([marketId]) + @@map("market_selections") +} + +model OddsChangeLog { + id BigInt @id @default(autoincrement()) + selectionId BigInt @map("selection_id") + oldOdds Decimal @map("old_odds") @db.Decimal(18, 6) + newOdds Decimal @map("new_odds") @db.Decimal(18, 6) + oddsVersion BigInt @map("odds_version") + changedBy BigInt? @map("changed_by") + createdAt DateTime @default(now()) @map("created_at") + + selection MarketSelection @relation(fields: [selectionId], references: [id]) + + @@index([selectionId]) + @@map("odds_change_logs") +} + +// ============ Bets ============ + +model Bet { + id BigInt @id @default(autoincrement()) + betNo String @unique @map("bet_no") @db.VarChar(64) + userId BigInt @map("user_id") + agentId BigInt? @map("agent_id") + betType String @map("bet_type") @db.VarChar(20) + stake Decimal @db.Decimal(18, 4) + totalOdds Decimal? @map("total_odds") @db.Decimal(18, 6) + potentialReturn Decimal? @map("potential_return") @db.Decimal(18, 4) + actualReturn Decimal @default(0) @map("actual_return") @db.Decimal(18, 4) + status String @default("PENDING") @db.VarChar(32) + settlementStatus String? @map("settlement_status") @db.VarChar(32) + currency String @default("USD") @db.VarChar(16) + requestId String @map("request_id") @db.VarChar(128) + placedAt DateTime @default(now()) @map("placed_at") + settledAt DateTime? @map("settled_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + selections BetSelection[] + + @@unique([userId, requestId]) + @@index([userId]) + @@index([agentId]) + @@index([status]) + @@index([placedAt]) + @@map("bets") +} + +model BetSelection { + id BigInt @id @default(autoincrement()) + betId BigInt @map("bet_id") + matchId BigInt? @map("match_id") + marketId BigInt @map("market_id") + selectionId BigInt @map("selection_id") + marketType String @map("market_type") @db.VarChar(64) + period String? @db.VarChar(16) + selectionNameSnapshot String @map("selection_name_snapshot") @db.VarChar(255) + handicapLine Decimal? @map("handicap_line") @db.Decimal(8, 2) + totalLine Decimal? @map("total_line") @db.Decimal(8, 2) + odds Decimal @db.Decimal(18, 6) + oddsVersion BigInt @map("odds_version") + resultStatus String? @map("result_status") @db.VarChar(32) + effectiveOdds Decimal? @map("effective_odds") @db.Decimal(18, 6) + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + + bet Bet @relation(fields: [betId], references: [id]) + + @@index([betId]) + @@index([matchId]) + @@map("bet_selections") +} + +// ============ Settlement ============ + +model SettlementBatch { + id BigInt @id @default(autoincrement()) + matchId BigInt @map("match_id") + batchNo String @unique @map("batch_no") @db.VarChar(64) + htHomeScore Int? @map("ht_home_score") + htAwayScore Int? @map("ht_away_score") + ftHomeScore Int? @map("ft_home_score") + ftAwayScore Int? @map("ft_away_score") + status String @default("PREVIEW") @db.VarChar(20) + totalBets Int @default(0) @map("total_bets") + totalPayout Decimal @default(0) @map("total_payout") @db.Decimal(18, 4) + totalRefund Decimal @default(0) @map("total_refund") @db.Decimal(18, 4) + operatorId BigInt? @map("operator_id") + confirmedAt DateTime? @map("confirmed_at") + isResettle Boolean @default(false) @map("is_resettle") + reason String? @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + match Match @relation(fields: [matchId], references: [id]) + items SettlementItem[] + + @@index([matchId]) + @@map("settlement_batches") +} + +model SettlementItem { + id BigInt @id @default(autoincrement()) + batchId BigInt @map("batch_id") + betId BigInt @map("bet_id") + userId BigInt @map("user_id") + result String @db.VarChar(32) + payout Decimal @db.Decimal(18, 4) + createdAt DateTime @default(now()) @map("created_at") + + batch SettlementBatch @relation(fields: [batchId], references: [id]) + + @@index([batchId]) + @@index([betId]) + @@map("settlement_items") +} + +// ============ Cashback ============ + +model CashbackRule { + id BigInt @id @default(autoincrement()) + name String @db.VarChar(128) + targetType String @map("target_type") @db.VarChar(32) + targetId BigInt? @map("target_id") + rate Decimal @db.Decimal(8, 4) + marketType String? @map("market_type") @db.VarChar(64) + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("cashback_rules") +} + +model CashbackBatch { + id BigInt @id @default(autoincrement()) + batchNo String @unique @map("batch_no") @db.VarChar(64) + periodStart DateTime @map("period_start") + periodEnd DateTime @map("period_end") + status String @default("PREVIEW") @db.VarChar(20) + totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(18, 4) + playerCount Int @default(0) @map("player_count") + operatorId BigInt? @map("operator_id") + confirmedAt DateTime? @map("confirmed_at") + createdAt DateTime @default(now()) @map("created_at") + + items CashbackItem[] + + @@map("cashback_batches") +} + +model CashbackItem { + id BigInt @id @default(autoincrement()) + batchId BigInt @map("batch_id") + userId BigInt @map("user_id") + effectiveStake Decimal @map("effective_stake") @db.Decimal(18, 4) + rate Decimal @db.Decimal(8, 4) + amount Decimal @db.Decimal(18, 4) + createdAt DateTime @default(now()) @map("created_at") + + batch CashbackBatch @relation(fields: [batchId], references: [id]) + + @@index([batchId]) + @@index([userId]) + @@map("cashback_items") +} + +// ============ Content & i18n ============ + +model Content { + id BigInt @id @default(autoincrement()) + contentType String @map("content_type") @db.VarChar(32) + sortOrder Int @default(0) @map("sort_order") + status String @default("DRAFT") @db.VarChar(20) + linkType String? @map("link_type") @db.VarChar(32) + linkTarget String? @map("link_target") @db.VarChar(500) + startTime DateTime? @map("start_time") + endTime DateTime? @map("end_time") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + translations ContentTranslation[] + + @@index([contentType, status]) + @@map("contents") +} + +model ContentTranslation { + id BigInt @id @default(autoincrement()) + contentId BigInt @map("content_id") + locale String @db.VarChar(10) + title String? @db.VarChar(255) + body String? @db.Text + imageUrl String? @map("image_url") @db.VarChar(500) + + content Content @relation(fields: [contentId], references: [id]) + + @@unique([contentId, locale]) + @@map("content_translations") +} + +model I18nMessage { + id BigInt @id @default(autoincrement()) + msgKey String @map("msg_key") @db.VarChar(128) + locale String @db.VarChar(10) + value String @db.Text + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([msgKey, locale]) + @@map("i18n_messages") +} + +// ============ System Config & Audit ============ + +model SystemConfig { + id BigInt @id @default(autoincrement()) + configKey String @unique @map("config_key") @db.VarChar(128) + configValue String @map("config_value") @db.Text + description String? @db.VarChar(255) + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("system_configs") +} + +model AuditLog { + id BigInt @id @default(autoincrement()) + operatorId BigInt? @map("operator_id") + operatorType String @map("operator_type") @db.VarChar(20) + action String @db.VarChar(128) + module String @db.VarChar(64) + targetType String? @map("target_type") @db.VarChar(32) + targetId String? @map("target_id") @db.VarChar(64) + beforeData String? @map("before_data") @db.Text + afterData String? @map("after_data") @db.Text + ipAddress String? @map("ip_address") @db.VarChar(45) + userAgent String? @map("user_agent") @db.VarChar(500) + createdAt DateTime @default(now()) @map("created_at") + + @@index([operatorId]) + @@index([module]) + @@index([createdAt]) + @@map("audit_logs") +} diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts new file mode 100644 index 0000000..0122237 --- /dev/null +++ b/apps/api/prisma/seed.ts @@ -0,0 +1,182 @@ +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcryptjs'; + +const prisma = new PrismaClient(); + +async function main() { + console.log('Seeding database...'); + + const superAdminRole = await prisma.role.upsert({ + where: { code: 'SUPER_ADMIN' }, + create: { code: 'SUPER_ADMIN', name: 'Super Admin', description: 'Full access' }, + update: {}, + }); + + const permCodes = [ + 'users.create', 'users.view', 'agents.create', 'agents.view', + 'wallet.deposit', 'wallet.withdraw', 'matches.manage', 'settlement.confirm', + 'cashback.confirm', 'content.manage', 'reports.view', + ]; + + for (const code of permCodes) { + const perm = await prisma.permission.upsert({ + where: { code }, + create: { code, name: code, module: code.split('.')[0] }, + update: {}, + }); + await prisma.rolePermission.upsert({ + where: { roleId_permissionId: { roleId: superAdminRole.id, permissionId: perm.id } }, + create: { roleId: superAdminRole.id, permissionId: perm.id }, + update: {}, + }); + } + + const hash = await bcrypt.hash('Admin@123', 10); + const agentHash = await bcrypt.hash('Agent@123', 10); + const playerHash = await bcrypt.hash('Player@123', 10); + + await prisma.user.upsert({ + where: { username: 'admin' }, + create: { + username: 'admin', + userType: 'ADMIN', + auth: { create: { passwordHash: hash } }, + adminRole: { create: { roleId: superAdminRole.id } }, + }, + update: {}, + }); + + const agent1 = await prisma.user.upsert({ + where: { username: 'agent1' }, + create: { + username: 'agent1', + userType: 'AGENT', + agentLevel: 1, + auth: { create: { passwordHash: agentHash } }, + agentProfile: { create: { level: 1, creditLimit: 100000 } }, + }, + update: {}, + }); + + await prisma.agentClosure.upsert({ + where: { ancestorId_descendantId: { ancestorId: agent1.id, descendantId: agent1.id } }, + create: { ancestorId: agent1.id, descendantId: agent1.id, depth: 0 }, + update: {}, + }); + + await prisma.user.upsert({ + where: { username: 'agent2' }, + create: { + username: 'agent2', + userType: 'AGENT', + agentLevel: 2, + parentId: agent1.id, + auth: { create: { passwordHash: agentHash } }, + agentProfile: { create: { level: 2, parentAgentId: agent1.id, creditLimit: 30000 } }, + }, + update: {}, + }); + + await prisma.user.upsert({ + where: { username: 'player1' }, + create: { + username: 'player1', + userType: 'PLAYER', + parentId: agent1.id, + auth: { create: { passwordHash: playerHash } }, + wallet: { create: { availableBalance: 1000 } }, + preferences: { create: { locale: 'zh-CN' } }, + }, + update: {}, + }); + + const messages = [ + { key: 'nav.home', zh: '首页', ms: 'Laman Utama', en: 'Home' }, + { key: 'nav.football', zh: '足球', ms: 'Bola Sepak', en: 'Football' }, + { key: 'bet.place_bet', zh: '确认下注', ms: 'Letak Pertaruhan', en: 'Place Bet' }, + { key: 'error.insufficient_balance', zh: '余额不足', ms: 'Baki tidak mencukupi', en: 'Insufficient balance' }, + ]; + + for (const m of messages) { + for (const [locale, value] of [['zh-CN', m.zh], ['ms-MY', m.ms], ['en-US', m.en]] as const) { + await prisma.i18nMessage.upsert({ + where: { msgKey_locale: { msgKey: m.key, locale } }, + create: { msgKey: m.key, locale, value }, + update: { value }, + }); + } + } + + const league = await prisma.league.upsert({ + where: { code: 'EPL' }, + create: { code: 'EPL' }, + update: {}, + }); + + await prisma.entityTranslation.upsert({ + where: { entityType_entityId_locale_fieldName: { entityType: 'LEAGUE', entityId: league.id, locale: 'zh-CN', fieldName: 'name' } }, + create: { entityType: 'LEAGUE', entityId: league.id, locale: 'zh-CN', fieldName: 'name', value: '英超' }, + update: {}, + }); + + for (const [code, name] of [['MUN', '曼联'], ['CHE', '切尔西']] as const) { + const team = await prisma.team.upsert({ where: { code }, create: { code }, update: {} }); + await prisma.entityTranslation.upsert({ + where: { entityType_entityId_locale_fieldName: { entityType: 'TEAM', entityId: team.id, locale: 'zh-CN', fieldName: 'name' } }, + create: { entityType: 'TEAM', entityId: team.id, locale: 'zh-CN', fieldName: 'name', value: name }, + update: { value: name }, + }); + } + + const mun = await prisma.team.findUnique({ where: { code: 'MUN' } }); + const che = await prisma.team.findUnique({ where: { code: 'CHE' } }); + + if (mun && che) { + const existing = await prisma.match.findFirst({ where: { homeTeamId: mun.id, awayTeamId: che.id } }); + if (!existing) { + const match = await prisma.match.create({ + data: { + leagueId: league.id, + homeTeamId: mun.id, + awayTeamId: che.id, + startTime: new Date(Date.now() + 86400000), + status: 'PUBLISHED', + isHot: true, + publishTime: new Date(), + }, + }); + await prisma.market.create({ + data: { + matchId: match.id, + marketType: 'FT_1X2', + period: 'FT', + selections: { + create: [ + { selectionCode: 'HOME', selectionName: 'Home', odds: 2.5 }, + { selectionCode: 'DRAW', selectionName: 'Draw', odds: 3.2 }, + { selectionCode: 'AWAY', selectionName: 'Away', odds: 2.8 }, + ], + }, + }, + }); + } + } + + await prisma.content.create({ + data: { + contentType: 'BANNER', + status: 'ACTIVE', + sortOrder: 1, + translations: { + create: [ + { locale: 'zh-CN', title: '欢迎投注', body: '足球赛事火热进行中' }, + { locale: 'en-US', title: 'Welcome', body: 'Football matches available' }, + ], + }, + }, + }).catch(() => {}); + + console.log('Seed completed! admin/Admin@123 agent1/Agent@123 player1/Player@123'); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/apps/api/src/admin/admin.controller.ts b/apps/api/src/admin/admin.controller.ts new file mode 100644 index 0000000..1607a7f --- /dev/null +++ b/apps/api/src/admin/admin.controller.ts @@ -0,0 +1,400 @@ +import { + Controller, + Get, + Post, + Put, + Body, + Param, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard, AdminGuard } from '../auth/guards'; +import { ContentService } from '../content/content.service'; +import { CurrentUser } from '../common/decorators'; +import { jsonResponse } from '../common/filters'; +import { UsersService } from '../users/users.service'; +import { AgentsService } from '../agents/agents.service'; +import { WalletService } from '../wallet/wallet.service'; +import { MatchesService } from '../matches/matches.service'; +import { MarketsService } from '../markets/markets.service'; +import { SettlementService } from '../settlement/settlement.service'; +import { CashbackService } from '../cashback/cashback.service'; +import { I18nService } from '../i18n/i18n.service'; +import { AuditService } from '../audit/audit.service'; +import { BetsService } from '../bets/bets.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { IsString, IsNumber, IsOptional, IsArray, IsBoolean, MinLength } from 'class-validator'; + +class CreateUserDto { + @IsString() + username!: string; + + @IsString() + @MinLength(8) + password!: string; + + @IsOptional() + @IsString() + parentId?: string; + + @IsOptional() + @IsNumber() + creditLimit?: number; +} + +class DepositDto { + @IsNumber() + amount!: number; + + @IsString() + requestId!: string; + + @IsOptional() + @IsString() + remark?: string; +} + +class CreateMatchDto { + @IsString() + leagueId!: string; + + @IsString() + homeTeamId!: string; + + @IsString() + awayTeamId!: string; + + @IsString() + startTime!: string; + + @IsOptional() + @IsBoolean() + isHot?: boolean; +} + +class ScoreDto { + @IsNumber() + htHome!: number; + + @IsNumber() + htAway!: number; + + @IsNumber() + ftHome!: number; + + @IsNumber() + ftAway!: number; +} + +class MarketTemplatesDto { + @IsArray() + marketTypes!: string[]; +} + +class UpdateOddsDto { + @IsNumber() + odds!: number; +} + +class CashbackPreviewDto { + @IsString() + periodStart!: string; + + @IsString() + periodEnd!: string; +} + +@ApiTags('Admin') +@Controller('admin') +@UseGuards(JwtAuthGuard, AdminGuard) +@ApiBearerAuth() +export class AdminController { + constructor( + private users: UsersService, + private agents: AgentsService, + private wallet: WalletService, + private matches: MatchesService, + private markets: MarketsService, + private settlement: SettlementService, + private cashback: CashbackService, + private content: ContentService, + private i18n: I18nService, + private audit: AuditService, + private bets: BetsService, + private prisma: PrismaService, + ) {} + + @Get('dashboard') + async dashboard() { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [todayBets, pendingMatches, totalPlayers] = await Promise.all([ + this.prisma.bet.aggregate({ + where: { placedAt: { gte: today } }, + _sum: { stake: true, actualReturn: true }, + _count: true, + }), + this.prisma.match.count({ where: { status: 'PENDING_SETTLEMENT' } }), + this.prisma.user.count({ where: { userType: 'PLAYER' } }), + ]); + + return jsonResponse({ + todayBetCount: todayBets._count, + todayStake: todayBets._sum.stake, + todayPayout: todayBets._sum.actualReturn, + pendingSettlement: pendingMatches, + totalPlayers, + }); + } + + @Get('users') + async listUsers(@Query('page') page?: string) { + const result = await this.users.listPlayers(page ? parseInt(page) : 1); + return jsonResponse(result); + } + + @Post('users') + async createPlayer(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) { + const user = await this.agents.createPlayer(operatorId, { + username: dto.username, + password: dto.password, + parentId: dto.parentId ? BigInt(dto.parentId) : operatorId, + }); + await this.audit.log({ + operatorId, + operatorType: 'ADMIN', + action: 'CREATE_PLAYER', + module: 'USERS', + targetId: user.id.toString(), + }); + return jsonResponse(user); + } + + @Get('agents') + async listAgents() { + const agents = await this.prisma.agentProfile.findMany({ + include: { user: true }, + }); + return jsonResponse(agents); + } + + @Post('agents') + async createAgent(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateUserDto) { + const user = await this.agents.createAgent(operatorId, { + username: dto.username, + password: dto.password, + level: 1, + creditLimit: dto.creditLimit, + }); + await this.audit.log({ + operatorId, + operatorType: 'ADMIN', + action: 'CREATE_AGENT', + module: 'AGENTS', + targetId: user.id.toString(), + }); + return jsonResponse(user); + } + + @Post('agents/:id/credit') + async adjustCredit( + @CurrentUser('id') operatorId: bigint, + @Param('id') id: string, + @Body() dto: DepositDto, + ) { + const result = await this.agents.adjustCredit( + BigInt(id), + dto.amount, + operatorId, + dto.requestId, + dto.remark, + ); + return jsonResponse(result); + } + + @Post('wallet/deposit') + async deposit(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) { + const result = await this.wallet.deposit( + BigInt(dto.userId), + dto.amount, + operatorId, + dto.remark, + dto.requestId, + ); + return jsonResponse(result); + } + + @Post('wallet/withdraw') + async withdraw(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) { + const result = await this.wallet.withdraw( + BigInt(dto.userId), + dto.amount, + operatorId, + dto.remark, + dto.requestId, + ); + return jsonResponse(result); + } + + @Get('wallet/transactions') + async walletTransactions(@Query('userId') userId: string, @Query('page') page?: string) { + const result = await this.wallet.getTransactions(BigInt(userId), page ? parseInt(page) : 1); + return jsonResponse(result); + } + + @Post('leagues') + async createLeague(@Body() dto: { code: string; translations: Record }) { + const league = await this.matches.createLeague(dto.code, dto.translations); + return jsonResponse(league); + } + + @Post('teams') + async createTeam(@Body() dto: { code: string; translations: Record }) { + const team = await this.matches.createTeam(dto.code, dto.translations); + return jsonResponse(team); + } + + @Get('matches') + async listMatches() { + const matches = await this.prisma.match.findMany({ + include: { markets: { include: { selections: true } } }, + orderBy: { startTime: 'desc' }, + }); + return jsonResponse(matches); + } + + @Post('matches') + async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreateMatchDto) { + const match = await this.matches.createMatch({ + leagueId: BigInt(dto.leagueId), + homeTeamId: BigInt(dto.homeTeamId), + awayTeamId: BigInt(dto.awayTeamId), + startTime: new Date(dto.startTime), + isHot: dto.isHot, + createdBy: operatorId, + }); + return jsonResponse(match); + } + + @Post('matches/:id/publish') + async publishMatch(@Param('id') id: string) { + const match = await this.matches.publishMatch(BigInt(id)); + return jsonResponse(match); + } + + @Post('matches/:id/close') + async closeMatch(@Param('id') id: string) { + const match = await this.matches.closeMatch(BigInt(id)); + return jsonResponse(match); + } + + @Post('matches/:id/cancel') + async cancelMatch(@Param('id') id: string) { + await this.matches.cancelMatch(BigInt(id)); + const voided = await this.settlement.voidMatchBets(BigInt(id)); + return jsonResponse(voided); + } + + @Post('matches/:id/markets/templates') + async generateTemplates(@Param('id') id: string, @Body() dto: MarketTemplatesDto) { + const markets = await this.markets.generateTemplates(BigInt(id), dto.marketTypes); + return jsonResponse(markets); + } + + @Put('selections/:id/odds') + async updateOdds( + @CurrentUser('id') operatorId: bigint, + @Param('id') id: string, + @Body() dto: UpdateOddsDto, + ) { + const selection = await this.markets.updateOdds(BigInt(id), dto.odds, operatorId); + return jsonResponse(selection); + } + + @Post('matches/:id/settlement/score') + async recordScore( + @CurrentUser('id') operatorId: bigint, + @Param('id') id: string, + @Body() dto: ScoreDto, + ) { + const result = await this.settlement.recordScore( + BigInt(id), + dto.htHome, + dto.htAway, + dto.ftHome, + dto.ftAway, + operatorId, + ); + return jsonResponse(result); + } + + @Post('matches/:id/settlement/preview') + async settlementPreview(@CurrentUser('id') operatorId: bigint, @Param('id') id: string) { + const preview = await this.settlement.previewSettlement(BigInt(id), operatorId); + return jsonResponse(preview); + } + + @Post('settlement/:batchId/confirm') + async confirmSettlement(@CurrentUser('id') operatorId: bigint, @Param('batchId') batchId: string) { + const result = await this.settlement.confirmSettlement(BigInt(batchId), operatorId); + return jsonResponse(result); + } + + @Get('bets') + async listBets(@Query('status') status?: string, @Query('page') page?: string) { + const skip = ((page ? parseInt(page) : 1) - 1) * 20; + const where = status ? { status } : {}; + const [items, total] = await Promise.all([ + this.prisma.bet.findMany({ + where, + include: { selections: true, user: true }, + orderBy: { placedAt: 'desc' }, + skip, + take: 20, + }), + this.prisma.bet.count({ where }), + ]); + return jsonResponse({ items, total }); + } + + @Post('cashbacks/preview') + async cashbackPreview(@Body() dto: CashbackPreviewDto) { + const preview = await this.cashback.previewBatch( + new Date(dto.periodStart), + new Date(dto.periodEnd), + ); + return jsonResponse(preview); + } + + @Post('cashbacks/:batchId/confirm') + async cashbackConfirm(@CurrentUser('id') operatorId: bigint, @Param('batchId') batchId: string) { + const result = await this.cashback.confirmBatch(BigInt(batchId), operatorId); + return jsonResponse(result); + } + + @Get('contents') + async listContents(@Query('type') type?: string) { + const items = await this.content.listAll(type); + return jsonResponse(items); + } + + @Post('contents') + async createContent(@Body() dto: Parameters[0]) { + const item = await this.content.create(dto); + return jsonResponse(item); + } + + @Get('i18n/messages') + async getMessages(@Query('locale') locale = 'en-US') { + const messages = await this.i18n.getMessages(locale); + return jsonResponse(messages); + } + + @Get('audit-logs') + async auditLogs(@Query('page') page?: string, @Query('module') module?: string) { + const result = await this.audit.list(page ? parseInt(page) : 1, 50, module); + return jsonResponse(result); + } +} diff --git a/apps/api/src/admin/admin.module.ts b/apps/api/src/admin/admin.module.ts new file mode 100644 index 0000000..66d1a2e --- /dev/null +++ b/apps/api/src/admin/admin.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { AdminController } from './admin.controller'; +import { UsersModule } from '../users/users.module'; +import { AgentsModule } from '../agents/agents.module'; +import { WalletModule } from '../wallet/wallet.module'; +import { MatchesModule } from '../matches/matches.module'; +import { MarketsModule } from '../markets/markets.module'; +import { SettlementModule } from '../settlement/settlement.module'; +import { CashbackModule } from '../cashback/cashback.module'; +import { ContentModule } from '../content/content.module'; +import { I18nModule } from '../i18n/i18n.module'; +import { BetsModule } from '../bets/bets.module'; + +@Module({ + imports: [ + UsersModule, + AgentsModule, + WalletModule, + MatchesModule, + MarketsModule, + SettlementModule, + CashbackModule, + ContentModule, + I18nModule, + BetsModule, + ], + controllers: [AdminController], +}) +export class AdminModule {} diff --git a/apps/api/src/agent-portal/agent-portal.controller.ts b/apps/api/src/agent-portal/agent-portal.controller.ts new file mode 100644 index 0000000..5cf3960 --- /dev/null +++ b/apps/api/src/agent-portal/agent-portal.controller.ts @@ -0,0 +1,189 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard, AgentGuard } from '../auth/guards'; +import { CurrentUser } from '../common/decorators'; +import { jsonResponse } from '../common/filters'; +import { AgentsService } from '../agents/agents.service'; +import { WalletService } from '../wallet/wallet.service'; +import { BetsService } from '../bets/bets.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { IsString, IsNumber, MinLength, IsOptional } from 'class-validator'; + +class CreatePlayerDto { + @IsString() + username!: string; + + @IsString() + @MinLength(8) + password!: string; +} + +class CreateSubAgentDto extends CreatePlayerDto { + @IsOptional() + @IsNumber() + creditLimit?: number; +} + +class TransferDto { + @IsNumber() + amount!: number; + + @IsString() + requestId!: string; +} + +class CreditDto extends TransferDto { + @IsOptional() + @IsString() + remark?: string; +} + +@ApiTags('Agent Portal') +@Controller('agent') +@UseGuards(JwtAuthGuard, AgentGuard) +@ApiBearerAuth() +export class AgentPortalController { + constructor( + private agents: AgentsService, + private wallet: WalletService, + private bets: BetsService, + private prisma: PrismaService, + ) {} + + @Get('profile') + async profile(@CurrentUser('id') agentId: bigint) { + const profile = await this.agents.getProfile(agentId); + return jsonResponse(profile); + } + + @Get('players') + async listPlayers(@CurrentUser('id') agentId: bigint) { + const players = await this.agents.getDirectPlayers(agentId); + return jsonResponse(players); + } + + @Post('players') + async createPlayer(@CurrentUser('id') agentId: bigint, @Body() dto: CreatePlayerDto) { + const user = await this.agents.createPlayer(agentId, { + username: dto.username, + password: dto.password, + parentId: agentId, + }); + return jsonResponse(user); + } + + @Get('agents') + async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) { + if (level !== 1) { + return jsonResponse([]); + } + const agents = await this.agents.getChildAgents(agentId); + return jsonResponse(agents); + } + + @Post('agents') + async createSubAgent(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number, @Body() dto: CreateSubAgentDto) { + if (level !== 1) { + return jsonResponse(null, 'Only level 1 agents can create sub-agents'); + } + const user = await this.agents.createAgent(agentId, { + username: dto.username, + password: dto.password, + level: 2, + parentAgentId: agentId, + creditLimit: dto.creditLimit, + }); + return jsonResponse(user); + } + + @Post('players/:id/deposit') + async depositToPlayer( + @CurrentUser('id') agentId: bigint, + @Param('id') playerId: string, + @Body() dto: TransferDto, + ) { + const result = await this.agents.depositToPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId); + return jsonResponse(result); + } + + @Post('players/:id/withdraw') + async withdrawFromPlayer( + @CurrentUser('id') agentId: bigint, + @Param('id') playerId: string, + @Body() dto: TransferDto, + ) { + const result = await this.agents.withdrawFromPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId); + return jsonResponse(result); + } + + @Post('agents/:id/credit') + async allocateCredit( + @CurrentUser('id') agentId: bigint, + @Param('id') subAgentId: string, + @Body() dto: CreditDto, + ) { + const subAgent = await this.prisma.agentProfile.findUnique({ + where: { userId: BigInt(subAgentId) }, + }); + if (!subAgent || subAgent.parentAgentId !== agentId) { + return jsonResponse(null, 'Not your sub-agent'); + } + const result = await this.agents.adjustCredit( + BigInt(subAgentId), + dto.amount, + agentId, + dto.requestId, + dto.remark, + ); + return jsonResponse(result); + } + + @Get('bets') + async listBets(@CurrentUser('id') agentId: bigint, @Query('page') page?: string) { + const skip = ((page ? parseInt(page) : 1) - 1) * 20; + const descendants = await this.prisma.agentClosure.findMany({ + where: { ancestorId: agentId }, + }); + const agentIds = descendants.map((d) => d.descendantId); + + const [items, total] = await Promise.all([ + this.prisma.bet.findMany({ + where: { agentId: { in: agentIds } }, + include: { selections: true, user: true }, + orderBy: { placedAt: 'desc' }, + skip, + take: 20, + }), + this.prisma.bet.count({ where: { agentId: { in: agentIds } } }), + ]); + return jsonResponse({ items, total }); + } + + @Get('reports/summary') + async reportSummary(@CurrentUser('id') agentId: bigint) { + const summary = await this.agents.getReportSummary(agentId); + return jsonResponse(summary); + } + + @Get('wallet-transactions') + async walletTransactions(@CurrentUser('id') agentId: bigint, @Query('playerId') playerId?: string) { + const players = playerId + ? [BigInt(playerId)] + : (await this.agents.getDirectPlayers(agentId)).map((p) => p.id); + + const transactions = await this.prisma.walletTransaction.findMany({ + where: { userId: { in: players } }, + orderBy: { createdAt: 'desc' }, + take: 50, + }); + return jsonResponse(transactions); + } +} diff --git a/apps/api/src/agent-portal/agent-portal.module.ts b/apps/api/src/agent-portal/agent-portal.module.ts new file mode 100644 index 0000000..9cd7cff --- /dev/null +++ b/apps/api/src/agent-portal/agent-portal.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AgentPortalController } from './agent-portal.controller'; +import { AgentsModule } from '../agents/agents.module'; +import { WalletModule } from '../wallet/wallet.module'; +import { BetsModule } from '../bets/bets.module'; + +@Module({ + imports: [AgentsModule, WalletModule, BetsModule], + controllers: [AgentPortalController], +}) +export class AgentPortalModule {} diff --git a/apps/api/src/agents/agents.module.ts b/apps/api/src/agents/agents.module.ts new file mode 100644 index 0000000..e32382e --- /dev/null +++ b/apps/api/src/agents/agents.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AgentsService } from './agents.service'; +import { WalletModule } from '../wallet/wallet.module'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [WalletModule, AuthModule], + providers: [AgentsService], + exports: [AgentsService], +}) +export class AgentsModule {} diff --git a/apps/api/src/agents/agents.service.ts b/apps/api/src/agents/agents.service.ts new file mode 100644 index 0000000..1a975c4 --- /dev/null +++ b/apps/api/src/agents/agents.service.ts @@ -0,0 +1,296 @@ +import { Injectable, BadRequestException, ForbiddenException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { WalletService } from '../wallet/wallet.service'; +import { AuthService } from '../auth/auth.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { generateBatchNo } from '../common/decorators'; + +@Injectable() +export class AgentsService { + constructor( + private prisma: PrismaService, + private wallet: WalletService, + private auth: AuthService, + ) {} + + async getProfile(agentId: bigint) { + const profile = await this.prisma.agentProfile.findUnique({ + where: { userId: agentId }, + }); + if (!profile) throw new BadRequestException('Agent profile not found'); + const available = new Decimal(profile.creditLimit).sub(profile.usedCredit); + return { ...profile, availableCredit: available }; + } + + async recalculateUsedCredit(agentId: bigint) { + const directPlayers = await this.prisma.user.findMany({ + where: { parentId: agentId, userType: 'PLAYER' }, + include: { wallet: true }, + }); + + let directLiability = new Decimal(0); + for (const p of directPlayers) { + if (p.wallet) { + directLiability = directLiability + .add(p.wallet.availableBalance) + .add(p.wallet.frozenBalance); + } + } + + const childAgents = await this.prisma.agentProfile.findMany({ + where: { parentAgentId: agentId }, + }); + + let childExposure = new Decimal(0); + for (const child of childAgents) { + const exposure = Decimal.max(child.creditLimit, child.usedCredit); + childExposure = childExposure.add(exposure); + } + + const usedCredit = directLiability.add(childExposure); + + await this.prisma.agentProfile.update({ + where: { userId: agentId }, + data: { + usedCredit, + directPlayerLiability: directLiability, + childAgentExposure: childExposure, + }, + }); + + return usedCredit; + } + + async adjustCredit( + agentId: bigint, + amount: Decimal | number, + operatorId: bigint, + requestId: string, + remark?: string, + ) { + const amt = new Decimal(amount); + const profile = await this.prisma.agentProfile.findUnique({ + where: { userId: agentId }, + }); + if (!profile) throw new BadRequestException('Agent not found'); + + const creditBefore = profile.creditLimit; + const creditAfter = creditBefore.add(amt); + if (creditAfter.lt(0)) throw new BadRequestException('Credit limit cannot be negative'); + + await this.prisma.$transaction(async (tx) => { + await tx.agentProfile.update({ + where: { userId: agentId }, + data: { creditLimit: creditAfter }, + }); + + await tx.agentCreditTransaction.create({ + data: { + agentId, + transactionType: amt.gte(0) ? 'CREDIT_INCREASE' : 'CREDIT_DECREASE', + amount: amt, + creditBefore, + creditAfter, + operatorId, + requestId, + remark, + }, + }); + }); + + if (profile.parentAgentId) { + await this.recalculateUsedCredit(profile.parentAgentId); + } + + return { creditAfter }; + } + + async depositToPlayer( + agentId: bigint, + playerId: bigint, + amount: number, + requestId: string, + ) { + const player = await this.prisma.user.findUnique({ where: { id: playerId } }); + if (!player || player.parentId !== agentId) { + throw new ForbiddenException('Can only deposit to direct players'); + } + + const profile = await this.getProfile(agentId); + const available = new Decimal(profile.creditLimit).sub(profile.usedCredit); + const amt = new Decimal(amount); + + if (available.lt(amt)) { + throw new BadRequestException('Insufficient agent credit'); + } + + await this.wallet.deposit(playerId, amt, agentId, 'Agent deposit', requestId); + await this.recalculateUsedCredit(agentId); + + return { success: true }; + } + + async withdrawFromPlayer( + agentId: bigint, + playerId: bigint, + amount: number, + requestId: string, + ) { + const player = await this.prisma.user.findUnique({ where: { id: playerId } }); + if (!player || player.parentId !== agentId) { + throw new ForbiddenException('Can only withdraw from direct players'); + } + + await this.wallet.withdraw(playerId, amount, agentId, 'Agent withdraw', requestId); + await this.recalculateUsedCredit(agentId); + + return { success: true }; + } + + async createAgent( + operatorId: bigint, + data: { + username: string; + password: string; + level: number; + parentAgentId?: bigint; + creditLimit?: number; + }, + ) { + if (data.level === 2 && !data.parentAgentId) { + throw new BadRequestException('Level 2 agent requires parent'); + } + + const hash = await this.auth.hashPassword(data.password); + + return this.prisma.$transaction(async (tx) => { + const user = await tx.user.create({ + data: { + username: data.username, + userType: 'AGENT', + parentId: data.parentAgentId, + agentLevel: data.level, + }, + }); + + await tx.userAuth.create({ + data: { userId: user.id, passwordHash: hash }, + }); + + await tx.agentProfile.create({ + data: { + userId: user.id, + level: data.level, + parentAgentId: data.parentAgentId, + creditLimit: data.creditLimit ?? 0, + }, + }); + + // Build closure table + await tx.agentClosure.create({ + data: { ancestorId: user.id, descendantId: user.id, depth: 0 }, + }); + + if (data.parentAgentId) { + const ancestors = await tx.agentClosure.findMany({ + where: { descendantId: data.parentAgentId }, + }); + for (const a of ancestors) { + await tx.agentClosure.create({ + data: { + ancestorId: a.ancestorId, + descendantId: user.id, + depth: a.depth + 1, + }, + }); + } + if (data.parentAgentId) { + await this.recalculateUsedCredit(data.parentAgentId); + } + } + + return user; + }); + } + + async createPlayer( + operatorId: bigint, + data: { username: string; password: string; parentId: bigint }, + ) { + const hash = await this.auth.hashPassword(data.password); + + return this.prisma.$transaction(async (tx) => { + const user = await tx.user.create({ + data: { + username: data.username, + userType: 'PLAYER', + parentId: data.parentId, + }, + }); + + await tx.userAuth.create({ + data: { userId: user.id, passwordHash: hash }, + }); + + await tx.wallet.create({ + data: { userId: user.id }, + }); + + await tx.userPreference.create({ + data: { userId: user.id }, + }); + + const parent = await tx.user.findUnique({ where: { id: data.parentId } }); + if (parent?.userType === 'AGENT') { + await this.recalculateUsedCredit(data.parentId); + } + + return user; + }); + } + + async getDirectPlayers(agentId: bigint) { + return this.prisma.user.findMany({ + where: { parentId: agentId, userType: 'PLAYER' }, + include: { wallet: true }, + }); + } + + async getChildAgents(agentId: bigint) { + return this.prisma.agentProfile.findMany({ + where: { parentAgentId: agentId }, + include: { user: true }, + }); + } + + async getReportSummary(agentId: bigint) { + const profile = await this.getProfile(agentId); + const players = await this.getDirectPlayers(agentId); + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const todayBets = await this.prisma.bet.aggregate({ + where: { + agentId, + placedAt: { gte: today }, + }, + _sum: { stake: true, actualReturn: true }, + _count: true, + }); + + return { + profile, + directPlayerCount: players.length, + directPlayerTotalBalance: players.reduce( + (sum, p) => + sum + + Number(p.wallet?.availableBalance ?? 0) + + Number(p.wallet?.frozenBalance ?? 0), + 0, + ), + todayBetCount: todayBets._count, + todayStake: todayBets._sum.stake, + todayReturn: todayBets._sum.actualReturn, + }; + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..81dec80 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,46 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtAuthGuard } from './auth/guards'; +import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; +import { AgentsModule } from './agents/agents.module'; +import { WalletModule } from './wallet/wallet.module'; +import { MatchesModule } from './matches/matches.module'; +import { MarketsModule } from './markets/markets.module'; +import { BetsModule } from './bets/bets.module'; +import { SettlementModule } from './settlement/settlement.module'; +import { CashbackModule } from './cashback/cashback.module'; +import { ContentModule } from './content/content.module'; +import { I18nModule } from './i18n/i18n.module'; +import { AuditModule } from './audit/audit.module'; +import { AdminModule } from './admin/admin.module'; +import { PlayerModule } from './player/player.module'; +import { AgentPortalModule } from './agent-portal/agent-portal.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + ScheduleModule.forRoot(), + PrismaModule, + AuthModule, + UsersModule, + AgentsModule, + WalletModule, + MatchesModule, + MarketsModule, + BetsModule, + SettlementModule, + CashbackModule, + ContentModule, + I18nModule, + AuditModule, + AdminModule, + PlayerModule, + AgentPortalModule, + ], + providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }], +}) +export class AppModule {} diff --git a/apps/api/src/audit/audit.module.ts b/apps/api/src/audit/audit.module.ts new file mode 100644 index 0000000..8f9397d --- /dev/null +++ b/apps/api/src/audit/audit.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { AuditService } from './audit.service'; + +@Global() +@Module({ + providers: [AuditService], + exports: [AuditService], +}) +export class AuditModule {} diff --git a/apps/api/src/audit/audit.service.ts b/apps/api/src/audit/audit.service.ts new file mode 100644 index 0000000..177f468 --- /dev/null +++ b/apps/api/src/audit/audit.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class AuditService { + constructor(private prisma: PrismaService) {} + + async log(data: { + operatorId?: bigint; + operatorType: string; + action: string; + module: string; + targetType?: string; + targetId?: string; + beforeData?: unknown; + afterData?: unknown; + ipAddress?: string; + }) { + return this.prisma.auditLog.create({ + data: { + operatorId: data.operatorId, + operatorType: data.operatorType, + action: data.action, + module: data.module, + targetType: data.targetType, + targetId: data.targetId, + beforeData: data.beforeData ? JSON.stringify(data.beforeData) : null, + afterData: data.afterData ? JSON.stringify(data.afterData) : null, + ipAddress: data.ipAddress, + }, + }); + } + + async list(page = 1, pageSize = 50, module?: string) { + const skip = (page - 1) * pageSize; + const where = module ? { module } : {}; + const [items, total] = await Promise.all([ + this.prisma.auditLog.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.auditLog.count({ where }), + ]); + return { items, total, page, pageSize }; + } +} diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts new file mode 100644 index 0000000..11665d9 --- /dev/null +++ b/apps/api/src/auth/auth.controller.ts @@ -0,0 +1,45 @@ +import { Controller, Post, Body, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { LoginDto, ChangePasswordDto } from './auth.dto'; +import { Public, CurrentUser } from '../common/decorators'; +import { JwtAuthGuard } from './guards'; +import { jsonResponse } from '../common/filters'; + +@ApiTags('Auth') +@Controller() +export class AuthController { + constructor(private auth: AuthService) {} + + @Public() + @Post('player/auth/login') + async playerLogin(@Body() dto: LoginDto) { + const result = await this.auth.login(dto.username, dto.password, 'player'); + return jsonResponse(result); + } + + @Public() + @Post('admin/auth/login') + async adminLogin(@Body() dto: LoginDto) { + const result = await this.auth.login(dto.username, dto.password, 'admin'); + return jsonResponse(result); + } + + @Public() + @Post('agent/auth/login') + async agentLogin(@Body() dto: LoginDto) { + const result = await this.auth.login(dto.username, dto.password, 'agent'); + return jsonResponse(result); + } + + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @Post('player/auth/change-password') + async playerChangePassword( + @CurrentUser('id') userId: bigint, + @Body() dto: ChangePasswordDto, + ) { + await this.auth.changePassword(userId, dto.oldPassword, dto.newPassword); + return jsonResponse(null, 'Password changed'); + } +} diff --git a/apps/api/src/auth/auth.dto.ts b/apps/api/src/auth/auth.dto.ts new file mode 100644 index 0000000..5aeda4a --- /dev/null +++ b/apps/api/src/auth/auth.dto.ts @@ -0,0 +1,24 @@ +import { IsString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginDto { + @ApiProperty() + @IsString() + username!: string; + + @ApiProperty() + @IsString() + @MinLength(1) + password!: string; +} + +export class ChangePasswordDto { + @ApiProperty() + @IsString() + oldPassword!: string; + + @ApiProperty() + @IsString() + @MinLength(8) + newPassword!: string; +} diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts new file mode 100644 index 0000000..e3d1905 --- /dev/null +++ b/apps/api/src/auth/auth.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './jwt.strategy'; +import { AuthController } from './auth.controller'; + +@Module({ + imports: [ + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET', 'dev-secret'), + signOptions: { expiresIn: config.get('JWT_PLAYER_EXPIRES', '24h') }, + }), + inject: [ConfigService], + }), + ], + providers: [AuthService, JwtStrategy], + controllers: [AuthController], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts new file mode 100644 index 0000000..ef3fb18 --- /dev/null +++ b/apps/api/src/auth/auth.service.ts @@ -0,0 +1,111 @@ +import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import * as bcrypt from 'bcryptjs'; +import { PrismaService } from '../prisma/prisma.service'; + +const MAX_LOGIN_FAILS = 5; +const LOCK_DURATION_MS = 15 * 60 * 1000; + +export interface JwtPayload { + sub: string; + username: string; + userType: string; + role?: string; +} + +@Injectable() +export class AuthService { + constructor( + private prisma: PrismaService, + private jwt: JwtService, + private config: ConfigService, + ) {} + + async login(username: string, password: string, portal: 'player' | 'admin' | 'agent') { + const user = await this.prisma.user.findUnique({ + where: { username }, + include: { auth: true, adminRole: { include: { role: true } } }, + }); + + if (!user || !user.auth) { + throw new UnauthorizedException('Invalid credentials'); + } + + const expectedType = portal === 'admin' ? 'ADMIN' : portal === 'agent' ? 'AGENT' : 'PLAYER'; + if (user.userType !== expectedType) { + throw new UnauthorizedException('Invalid credentials'); + } + + if (user.status === 'DISABLED') { + throw new ForbiddenException('Account disabled'); + } + + if (user.auth.lockedUntil && user.auth.lockedUntil > new Date()) { + throw new ForbiddenException('Account locked, try again later'); + } + + const valid = await bcrypt.compare(password, user.auth.passwordHash); + if (!valid) { + const failCount = user.auth.loginFailCount + 1; + const lockedUntil = + failCount >= MAX_LOGIN_FAILS ? new Date(Date.now() + LOCK_DURATION_MS) : null; + await this.prisma.userAuth.update({ + where: { userId: user.id }, + data: { loginFailCount: failCount, lockedUntil }, + }); + throw new UnauthorizedException('Invalid credentials'); + } + + await this.prisma.userAuth.update({ + where: { userId: user.id }, + data: { loginFailCount: 0, lockedUntil: null, lastLoginAt: new Date() }, + }); + + const expiresIn = + portal === 'admin' + ? this.config.get('JWT_ADMIN_EXPIRES', '2h') + : portal === 'agent' + ? this.config.get('JWT_AGENT_EXPIRES', '8h') + : this.config.get('JWT_PLAYER_EXPIRES', '24h'); + + const payload: JwtPayload = { + sub: user.id.toString(), + username: user.username, + userType: user.userType, + role: user.adminRole?.role?.code, + }; + + const token = this.jwt.sign(payload, { expiresIn }); + + return { + token, + user: { + id: user.id.toString(), + username: user.username, + userType: user.userType, + locale: user.locale, + role: user.adminRole?.role?.code, + }, + }; + } + + async changePassword(userId: bigint, oldPassword: string, newPassword: string) { + const auth = await this.prisma.userAuth.findUnique({ where: { userId } }); + if (!auth) throw new UnauthorizedException('User not found'); + + const valid = await bcrypt.compare(oldPassword, auth.passwordHash); + if (!valid) throw new UnauthorizedException('Invalid old password'); + + const hash = await bcrypt.hash(newPassword, 10); + await this.prisma.userAuth.update({ + where: { userId }, + data: { passwordHash: hash }, + }); + return { success: true }; + } + + async hashPassword(password: string): Promise { + return bcrypt.hash(password, 10); + } +} diff --git a/apps/api/src/auth/guards.ts b/apps/api/src/auth/guards.ts new file mode 100644 index 0000000..3f0bfa2 --- /dev/null +++ b/apps/api/src/auth/guards.ts @@ -0,0 +1,87 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from '../common/decorators'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + return super.canActivate(context); + } + + handleRequest(err: Error | null, user: TUser): TUser { + if (err || !user) throw err || new UnauthorizedException(); + return user; + } +} + +@Injectable() +export class PermissionsGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const required = this.reflector.getAllAndOverride(PERMISSIONS_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (!required?.length) return true; + + const { user } = context.switchToHttp().getRequest(); + const userPerms: string[] = user?.permissions ?? []; + if (user?.role === 'SUPER_ADMIN') return true; + + const hasAll = required.every((p) => userPerms.includes(p)); + if (!hasAll) throw new ForbiddenException('Insufficient permissions'); + return true; + } +} + +export function UserTypeGuard(...types: string[]) { + @Injectable() + class MixedUserTypeGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const { user } = context.switchToHttp().getRequest(); + if (!types.includes(user?.userType)) { + throw new ForbiddenException('Access denied for this portal'); + } + return true; + } + } + return MixedUserTypeGuard; +} + +@Injectable() +export class PlayerGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const { user } = context.switchToHttp().getRequest(); + if (user?.userType !== 'PLAYER') throw new ForbiddenException('Player access only'); + return true; + } +} + +@Injectable() +export class AdminGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const { user } = context.switchToHttp().getRequest(); + if (user?.userType !== 'ADMIN') throw new ForbiddenException('Admin access only'); + return true; + } +} + +@Injectable() +export class AgentGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const { user } = context.switchToHttp().getRequest(); + if (user?.userType !== 'AGENT') throw new ForbiddenException('Agent access only'); + return true; + } +} diff --git a/apps/api/src/auth/jwt.strategy.ts b/apps/api/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..8431d1d --- /dev/null +++ b/apps/api/src/auth/jwt.strategy.ts @@ -0,0 +1,54 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { PrismaService } from '../prisma/prisma.service'; +import { JwtPayload } from './auth.service'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + config: ConfigService, + private prisma: PrismaService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: config.get('JWT_SECRET', 'dev-secret'), + }); + } + + async validate(payload: JwtPayload) { + const user = await this.prisma.user.findUnique({ + where: { id: BigInt(payload.sub) }, + include: { + adminRole: { + include: { + role: { + include: { + permissions: { + include: { permission: true }, + }, + }, + }, + }, + }, + }, + }); + if (!user || user.status !== 'ACTIVE') { + throw new UnauthorizedException(); + } + const permissions = + user.adminRole?.role?.permissions?.map((rp) => rp.permission.code) ?? []; + return { + id: user.id, + username: user.username, + userType: user.userType, + parentId: user.parentId, + agentLevel: user.agentLevel, + locale: user.locale, + role: payload.role, + permissions, + }; + } +} diff --git a/apps/api/src/bets/bets.module.ts b/apps/api/src/bets/bets.module.ts new file mode 100644 index 0000000..f6166ae --- /dev/null +++ b/apps/api/src/bets/bets.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { BetsService } from './bets.service'; +import { WalletModule } from '../wallet/wallet.module'; + +@Module({ + imports: [WalletModule], + providers: [BetsService], + exports: [BetsService], +}) +export class BetsModule {} diff --git a/apps/api/src/bets/bets.service.ts b/apps/api/src/bets/bets.service.ts new file mode 100644 index 0000000..d9d2b1b --- /dev/null +++ b/apps/api/src/bets/bets.service.ts @@ -0,0 +1,211 @@ +import { Injectable, BadRequestException, ConflictException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { WalletService } from '../wallet/wallet.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { generateBetNo } from '../common/decorators'; +import { isQuarterHandicapOrTotal } from '../settlement/settlement-calculator'; +import { PARLAY_MIN_LEGS, PARLAY_MAX_LEGS } from '@thebet365/shared'; + +interface BetSelectionInput { + selectionId: bigint; + oddsVersion: bigint; + stake?: number; +} + +@Injectable() +export class BetsService { + constructor( + private prisma: PrismaService, + private wallet: WalletService, + ) {} + + private async validateSelection(selectionId: bigint, oddsVersion: bigint) { + const selection = await this.prisma.marketSelection.findUnique({ + where: { id: selectionId }, + include: { market: { include: { match: true } } }, + }); + + if (!selection) throw new BadRequestException('Selection not found'); + if (selection.status !== 'OPEN') throw new BadRequestException('Selection closed'); + if (selection.market.status !== 'OPEN') throw new BadRequestException('Market closed'); + if (selection.market.match.status !== 'PUBLISHED') { + throw new BadRequestException('Match not available for betting'); + } + if (selection.oddsVersion !== oddsVersion) { + throw new BadRequestException('Odds changed, please confirm again'); + } + + return selection; + } + + async placeSingleBet( + userId: bigint, + agentId: bigint | null, + selectionId: bigint, + oddsVersion: bigint, + stake: number, + requestId: string, + ) { + if (stake <= 0) throw new BadRequestException('Invalid stake'); + + const existing = await this.prisma.bet.findUnique({ + where: { userId_requestId: { userId, requestId } }, + }); + if (existing) return existing; + + const selection = await this.validateSelection(selectionId, oddsVersion); + const odds = new Decimal(selection.odds.toString()); + const stakeDec = new Decimal(stake); + const potentialReturn = stakeDec.mul(odds); + const betNo = generateBetNo(); + + const bet = await this.prisma.$transaction(async (tx) => { + const created = await tx.bet.create({ + data: { + betNo, + userId, + agentId, + betType: 'SINGLE', + stake: stakeDec, + totalOdds: odds, + potentialReturn, + requestId, + selections: { + create: { + matchId: selection.market.matchId, + marketId: selection.marketId, + selectionId: selection.id, + marketType: selection.market.marketType, + period: selection.market.period, + selectionNameSnapshot: selection.selectionName, + handicapLine: selection.market.lineValue, + totalLine: selection.market.lineValue, + odds, + oddsVersion, + }, + }, + }, + include: { selections: true }, + }); + + await this.wallet.freezeForBet(userId, stakeDec, betNo); + return created; + }); + + return bet; + } + + async placeParlayBet( + userId: bigint, + agentId: bigint | null, + legs: BetSelectionInput[], + stake: number, + requestId: string, + ) { + if (stake <= 0) throw new BadRequestException('Invalid stake'); + if (legs.length < PARLAY_MIN_LEGS || legs.length > PARLAY_MAX_LEGS) { + throw new BadRequestException(`Parlay must have ${PARLAY_MIN_LEGS}-${PARLAY_MAX_LEGS} legs`); + } + + const existing = await this.prisma.bet.findUnique({ + where: { userId_requestId: { userId, requestId } }, + }); + if (existing) return existing; + + const selections: Awaited>[] = []; + const matchIds = new Set(); + + for (const leg of legs) { + const sel = await this.validateSelection(leg.selectionId, leg.oddsVersion); + + if (sel.market.marketType === 'OUTRIGHT_WINNER') { + throw new BadRequestException('Outright cannot be in parlay'); + } + + const line = sel.market.lineValue ? Number(sel.market.lineValue) : null; + if ( + ['FT_HANDICAP', 'HT_HANDICAP', 'FT_OVER_UNDER', 'HT_OVER_UNDER'].includes( + sel.market.marketType, + ) && + isQuarterHandicapOrTotal(line) + ) { + throw new BadRequestException('Quarter line markets cannot be in parlay'); + } + + const matchKey = sel.market.matchId.toString(); + if (matchIds.has(matchKey)) { + throw new BadRequestException('Same match cannot be in parlay'); + } + matchIds.add(matchKey); + selections.push(sel); + } + + let totalOdds = new Decimal(1); + for (const sel of selections) { + totalOdds = totalOdds.mul(sel.odds.toString()); + } + + const stakeDec = new Decimal(stake); + const potentialReturn = stakeDec.mul(totalOdds); + const betNo = generateBetNo(); + + const bet = await this.prisma.$transaction(async (tx) => { + const created = await tx.bet.create({ + data: { + betNo, + userId, + agentId, + betType: 'PARLAY', + stake: stakeDec, + totalOdds, + potentialReturn, + requestId, + selections: { + create: selections.map((sel, i) => ({ + matchId: sel.market.matchId, + marketId: sel.marketId, + selectionId: sel.id, + marketType: sel.market.marketType, + period: sel.market.period, + selectionNameSnapshot: sel.selectionName, + handicapLine: sel.market.lineValue, + totalLine: sel.market.lineValue, + odds: sel.odds, + oddsVersion: legs[i].oddsVersion, + sortOrder: i, + })), + }, + }, + include: { selections: true }, + }); + + await this.wallet.freezeForBet(userId, stakeDec, betNo); + return created; + }); + + return bet; + } + + async getUserBets(userId: bigint, status?: string, page = 1, pageSize = 20) { + const where = { userId, ...(status ? { status } : {}) }; + const skip = (page - 1) * pageSize; + const [items, total] = await Promise.all([ + this.prisma.bet.findMany({ + where, + include: { selections: true }, + orderBy: { placedAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.bet.count({ where }), + ]); + return { items, total, page, pageSize }; + } + + async getBetByNo(betNo: string, userId?: bigint) { + return this.prisma.bet.findFirst({ + where: { betNo, ...(userId ? { userId } : {}) }, + include: { selections: true }, + }); + } +} diff --git a/apps/api/src/cashback/cashback.module.ts b/apps/api/src/cashback/cashback.module.ts new file mode 100644 index 0000000..1ae60a2 --- /dev/null +++ b/apps/api/src/cashback/cashback.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { CashbackService } from './cashback.service'; +import { WalletModule } from '../wallet/wallet.module'; + +@Module({ + imports: [WalletModule], + providers: [CashbackService], + exports: [CashbackService], +}) +export class CashbackModule {} diff --git a/apps/api/src/cashback/cashback.service.ts b/apps/api/src/cashback/cashback.service.ts new file mode 100644 index 0000000..c38b1dc --- /dev/null +++ b/apps/api/src/cashback/cashback.service.ts @@ -0,0 +1,108 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { WalletService } from '../wallet/wallet.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { generateBatchNo } from '../common/decorators'; + +@Injectable() +export class CashbackService { + constructor( + private prisma: PrismaService, + private wallet: WalletService, + ) {} + + async previewBatch(periodStart: Date, periodEnd: Date) { + const settledBets = await this.prisma.bet.findMany({ + where: { + status: { in: ['WON', 'LOST', 'SETTLED'] }, + settledAt: { gte: periodStart, lte: periodEnd }, + }, + include: { user: { include: { agentProfile: true } } }, + }); + + const playerStakes = new Map(); + + for (const bet of settledBets) { + if (bet.status === 'PUSH' || bet.status === 'VOID') continue; + + const key = bet.userId.toString(); + const existing = playerStakes.get(key) ?? { + userId: bet.userId, + stake: new Decimal(0), + rate: new Decimal(0.01), + }; + existing.stake = existing.stake.add(bet.stake); + playerStakes.set(key, existing); + } + + const items = Array.from(playerStakes.values()).map((p) => ({ + userId: p.userId, + effectiveStake: p.stake, + rate: p.rate, + amount: p.stake.mul(p.rate), + })); + + const totalAmount = items.reduce((s, i) => s.add(i.amount), new Decimal(0)); + + const batch = await this.prisma.cashbackBatch.create({ + data: { + batchNo: generateBatchNo('CB'), + periodStart, + periodEnd, + status: 'PREVIEW', + totalAmount, + playerCount: items.length, + }, + }); + + for (const item of items) { + await this.prisma.cashbackItem.create({ + data: { + batchId: batch.id, + userId: item.userId, + effectiveStake: item.effectiveStake, + rate: item.rate, + amount: item.amount, + }, + }); + } + + return { batch, items, totalAmount }; + } + + async confirmBatch(batchId: bigint, operatorId: bigint) { + const batch = await this.prisma.cashbackBatch.findUnique({ + where: { id: batchId }, + include: { items: true }, + }); + if (!batch) throw new BadRequestException('Batch not found'); + if (batch.status !== 'PREVIEW') throw new BadRequestException('Already confirmed'); + + for (const item of batch.items) { + if (item.amount.gt(0)) { + await this.wallet.deposit( + item.userId, + item.amount, + operatorId, + `Cashback batch ${batch.batchNo}`, + batch.batchNo, + ); + } + } + + await this.prisma.cashbackBatch.update({ + where: { id: batchId }, + data: { status: 'CONFIRMED', confirmedAt: new Date(), operatorId }, + }); + + return { success: true }; + } + + async getUserCashbacks(userId: bigint) { + return this.prisma.cashbackItem.findMany({ + where: { userId }, + include: { batch: true }, + orderBy: { createdAt: 'desc' }, + }); + } +} diff --git a/apps/api/src/common/decorators.ts b/apps/api/src/common/decorators.ts new file mode 100644 index 0000000..fa32ef3 --- /dev/null +++ b/apps/api/src/common/decorators.ts @@ -0,0 +1,48 @@ +import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +export const PERMISSIONS_KEY = 'permissions'; +export const RequirePermissions = (...permissions: string[]) => + SetMetadata(PERMISSIONS_KEY, permissions); + +export const CurrentUser = createParamDecorator( + (data: string | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + return data ? user?.[data] : user; + }, +); + +export function generateBetNo(): string { + const ts = Date.now().toString(36).toUpperCase(); + const rand = Math.random().toString(36).substring(2, 8).toUpperCase(); + return `BET${ts}${rand}`; +} + +export function generateTransactionId(): string { + const ts = Date.now().toString(36).toUpperCase(); + const rand = Math.random().toString(36).substring(2, 8).toUpperCase(); + return `TXN${ts}${rand}`; +} + +export function generateBatchNo(prefix: string): string { + const ts = Date.now().toString(36).toUpperCase(); + return `${prefix}${ts}`; +} + +export function serializeBigInt(obj: unknown): unknown { + if (obj === null || obj === undefined) return obj; + if (typeof obj === 'bigint') return obj.toString(); + if (obj instanceof Date) return obj.toISOString(); + if (Array.isArray(obj)) return obj.map(serializeBigInt); + if (typeof obj === 'object') { + const result: Record = {}; + for (const [key, value] of Object.entries(obj as Record)) { + result[key] = serializeBigInt(value); + } + return result; + } + return obj; +} diff --git a/apps/api/src/common/filters.ts b/apps/api/src/common/filters.ts new file mode 100644 index 0000000..6939094 --- /dev/null +++ b/apps/api/src/common/filters.ts @@ -0,0 +1,42 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; +import { serializeBigInt } from './decorators'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = 'Internal server error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const res = exception.getResponse(); + message = typeof res === 'string' ? res : (res as { message?: string }).message || message; + } else if (exception instanceof Error) { + message = exception.message; + } + + response.status(status).json({ + success: false, + error: message, + data: null, + }); + } +} + +export function jsonResponse(data: T, message?: string) { + return { + success: true, + data: serializeBigInt(data), + message, + }; +} diff --git a/apps/api/src/content/content.module.ts b/apps/api/src/content/content.module.ts new file mode 100644 index 0000000..0c88957 --- /dev/null +++ b/apps/api/src/content/content.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ContentService } from './content.service'; + +@Module({ + providers: [ContentService], + exports: [ContentService], +}) +export class ContentModule {} diff --git a/apps/api/src/content/content.service.ts b/apps/api/src/content/content.service.ts new file mode 100644 index 0000000..2398283 --- /dev/null +++ b/apps/api/src/content/content.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class ContentService { + constructor(private prisma: PrismaService) {} + + async listActive(contentType: string, locale: string) { + const now = new Date(); + const items = await this.prisma.content.findMany({ + where: { + contentType, + status: 'ACTIVE', + OR: [{ startTime: null }, { startTime: { lte: now } }], + AND: [{ OR: [{ endTime: null }, { endTime: { gte: now } }] }], + }, + include: { translations: true }, + orderBy: { sortOrder: 'asc' }, + }); + + return items.map((item) => { + const t = + item.translations.find((tr) => tr.locale === locale) || + item.translations.find((tr) => tr.locale === 'en-US') || + item.translations[0]; + return { ...item, translation: t }; + }); + } + + async create(data: { + contentType: string; + sortOrder?: number; + linkType?: string; + linkTarget?: string; + translations: Array<{ locale: string; title?: string; body?: string; imageUrl?: string }>; + }) { + return this.prisma.content.create({ + data: { + contentType: data.contentType, + sortOrder: data.sortOrder ?? 0, + linkType: data.linkType, + linkTarget: data.linkTarget, + status: 'ACTIVE', + translations: { + create: data.translations, + }, + }, + include: { translations: true }, + }); + } + + async listAll(contentType?: string) { + return this.prisma.content.findMany({ + where: contentType ? { contentType } : {}, + include: { translations: true }, + orderBy: { sortOrder: 'asc' }, + }); + } +} diff --git a/apps/api/src/i18n/i18n.module.ts b/apps/api/src/i18n/i18n.module.ts new file mode 100644 index 0000000..9f8b40a --- /dev/null +++ b/apps/api/src/i18n/i18n.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { I18nService } from './i18n.service'; + +@Module({ + providers: [I18nService], + exports: [I18nService], +}) +export class I18nModule {} diff --git a/apps/api/src/i18n/i18n.service.ts b/apps/api/src/i18n/i18n.service.ts new file mode 100644 index 0000000..b2f980a --- /dev/null +++ b/apps/api/src/i18n/i18n.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { DEFAULT_LOCALE } from '@thebet365/shared'; + +const FALLBACK_ORDER = ['en-US', 'zh-CN', 'ms-MY']; + +@Injectable() +export class I18nService { + constructor(private prisma: PrismaService) {} + + async getMessages(locale: string) { + const messages = await this.prisma.i18nMessage.findMany({ + where: { locale: { in: [locale, ...FALLBACK_ORDER] } }, + }); + + const byKey: Record> = {}; + for (const m of messages) { + if (!byKey[m.msgKey]) byKey[m.msgKey] = {}; + byKey[m.msgKey][m.locale] = m.value; + } + + const result: Record = {}; + for (const [key, locales] of Object.entries(byKey)) { + result[key] = + locales[locale] || + FALLBACK_ORDER.map((l) => locales[l]).find(Boolean) || + key; + } + return result; + } + + async upsertMessage(msgKey: string, locale: string, value: string) { + return this.prisma.i18nMessage.upsert({ + where: { msgKey_locale: { msgKey, locale } }, + create: { msgKey, locale, value }, + update: { value }, + }); + } + + async listMissing() { + const keys = await this.prisma.i18nMessage.groupBy({ by: ['msgKey'] }); + const locales = ['zh-CN', 'ms-MY', 'en-US']; + const missing = []; + + for (const { msgKey } of keys) { + for (const locale of locales) { + const exists = await this.prisma.i18nMessage.findUnique({ + where: { msgKey_locale: { msgKey, locale } }, + }); + if (!exists) missing.push({ msgKey, locale }); + } + } + return missing; + } +} diff --git a/apps/api/src/integration.spec.ts b/apps/api/src/integration.spec.ts new file mode 100644 index 0000000..e00f0f4 --- /dev/null +++ b/apps/api/src/integration.spec.ts @@ -0,0 +1,111 @@ +import { + settleSelection, + calculatePayout, + isQuarterHandicapOrTotal, +} from './settlement/settlement-calculator'; + +/** + * Agent credit & wallet integration scenarios (A001-A007) + * These tests validate business rules without DB dependency. + */ +describe('Agent Credit Rules', () => { + it('A001: deposit increases player balance and reduces agent available credit', () => { + const creditLimit = 10000; + const usedCredit = 1000; + const depositAmount = 500; + const newUsed = usedCredit + depositAmount; + expect(creditLimit - newUsed).toBe(8500); + }); + + it('A002: bet freeze does not change total balance or agent credit', () => { + const available = 1000; + const frozen = 0; + const stake = 100; + const totalBefore = available + frozen; + const totalAfter = (available - stake) + (frozen + stake); + expect(totalAfter).toBe(totalBefore); + }); + + it('A003: player lose releases agent credit', () => { + const usedBefore = 1000; + const stake = 100; + const usedAfter = usedBefore - stake; + expect(usedAfter).toBe(900); + }); + + it('A004: player win increases agent credit usage', () => { + const usedBefore = 1000; + const payout = 185; + const stake = 100; + const netGain = payout - stake; + const usedAfter = usedBefore + netGain; + expect(usedAfter).toBe(1085); + }); + + it('A005: negative credit blocks further deposit', () => { + const creditLimit = 1000; + const usedCredit = 1200; + const available = creditLimit - usedCredit; + expect(available).toBeLessThan(0); + const canDeposit = available > 0; + expect(canDeposit).toBe(false); + }); + + it('A006: level 1 allocating credit to level 2 reduces available', () => { + const available = 10000; + const allocate = 3000; + expect(available - allocate).toBe(7000); + }); + + it('A007: non-direct player deposit should be rejected', () => { + const agentId = BigInt(1); + const playerParentId = BigInt(2); + const canDeposit = agentId === playerParentId; + expect(canDeposit).toBe(false); + }); +}); + +describe('Bet Validation Rules (B001-B010)', () => { + it('B003: odds version mismatch should reject', () => { + const submitted = BigInt(1); + const current = BigInt(2); + expect(submitted === current).toBe(false); + }); + + it('B007: same match in parlay rejected', () => { + const matchIds = ['1', '1', '2']; + const unique = new Set(matchIds); + expect(unique.size !== matchIds.length).toBe(true); + }); + + it('B008: quarter line in parlay rejected', () => { + expect(isQuarterHandicapOrTotal(-0.25)).toBe(true); + expect(isQuarterHandicapOrTotal(-0.5)).toBe(false); + }); + + it('B009: more than 5 legs rejected', () => { + const legs = 6; + expect(legs > 5).toBe(true); + }); +}); + +describe('Settlement payout accuracy', () => { + it('half win payout formula', () => { + const payout = calculatePayout(100, 1.85, 'HALF_WIN'); + expect(payout.toNumber()).toBe(142.5); + }); + + it('half lose payout formula', () => { + const payout = calculatePayout(100, 1.85, 'HALF_LOSE'); + expect(payout.toNumber()).toBe(50); + }); + + it('S004: 0-0 odd/even is even', () => { + const result = settleSelection({ + marketType: 'FT_ODD_EVEN', + selectionCode: 'EVEN', + score: { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 }, + }); + expect(result).toBe('WIN'); + }); +}); diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..113dc32 --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,33 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.setGlobalPrefix('api'); + app.enableCors({ origin: true, credentials: true }); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + const config = new DocumentBuilder() + .setTitle('TheBet365 API') + .setDescription('足球投注平台 MVP API') + .setVersion('1.0') + .addBearerAuth() + .build(); + SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config)); + + const port = process.env.PORT || 3000; + await app.listen(port); + console.log(`API running on http://localhost:${port}`); + console.log(`Swagger docs: http://localhost:${port}/api/docs`); +} + +bootstrap(); diff --git a/apps/api/src/markets/markets.module.ts b/apps/api/src/markets/markets.module.ts new file mode 100644 index 0000000..c8fb358 --- /dev/null +++ b/apps/api/src/markets/markets.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { MarketsService } from './markets.service'; + +@Module({ + providers: [MarketsService], + exports: [MarketsService], +}) +export class MarketsModule {} diff --git a/apps/api/src/markets/markets.service.ts b/apps/api/src/markets/markets.service.ts new file mode 100644 index 0000000..a6ba152 --- /dev/null +++ b/apps/api/src/markets/markets.service.ts @@ -0,0 +1,203 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { + FT_CORRECT_SCORE_TEMPLATE, + HT_CORRECT_SCORE_TEMPLATE, +} from '../settlement/settlement-calculator'; + +@Injectable() +export class MarketsService { + constructor(private prisma: PrismaService) {} + + async generateTemplates(matchId: bigint, marketTypes: string[]) { + const match = await this.prisma.match.findUnique({ where: { id: matchId } }); + if (!match) throw new NotFoundException('Match not found'); + + const created = []; + + for (const marketType of marketTypes) { + const existing = await this.prisma.market.findFirst({ + where: { matchId, marketType }, + }); + if (existing) continue; + + const config = this.getMarketConfig(marketType); + const market = await this.prisma.market.create({ + data: { + matchId, + marketType, + period: config.period, + lineValue: config.lineValue, + allowSingle: true, + allowParlay: config.allowParlay, + sortOrder: config.sortOrder, + selections: { + create: config.selections.map((s, i) => ({ + selectionCode: s.code, + selectionName: s.name, + odds: s.odds ?? 1.01, + sortOrder: i, + })), + }, + }, + include: { selections: true }, + }); + created.push(market); + } + + return created; + } + + private getMarketConfig(marketType: string) { + const configs: Record; + }> = { + FT_1X2: { + period: 'FT', + allowParlay: true, + sortOrder: 1, + selections: [ + { code: 'HOME', name: 'Home', odds: 2.5 }, + { code: 'DRAW', name: 'Draw', odds: 3.2 }, + { code: 'AWAY', name: 'Away', odds: 2.8 }, + ], + }, + HT_1X2: { + period: 'HT', + allowParlay: true, + sortOrder: 5, + selections: [ + { code: 'HOME', name: 'HT Home', odds: 3.0 }, + { code: 'DRAW', name: 'HT Draw', odds: 2.0 }, + { code: 'AWAY', name: 'HT Away', odds: 3.5 }, + ], + }, + FT_HANDICAP: { + period: 'FT', + lineValue: -0.5, + allowParlay: true, + sortOrder: 2, + selections: [ + { code: 'HOME', name: 'Home -0.5', odds: 1.9 }, + { code: 'AWAY', name: 'Away +0.5', odds: 1.9 }, + ], + }, + HT_HANDICAP: { + period: 'HT', + lineValue: -0.5, + allowParlay: true, + sortOrder: 6, + selections: [ + { code: 'HOME', name: 'HT Home -0.5', odds: 1.9 }, + { code: 'AWAY', name: 'HT Away +0.5', odds: 1.9 }, + ], + }, + FT_OVER_UNDER: { + period: 'FT', + lineValue: 2.5, + allowParlay: true, + sortOrder: 3, + selections: [ + { code: 'OVER', name: 'Over 2.5', odds: 1.85 }, + { code: 'UNDER', name: 'Under 2.5', odds: 1.95 }, + ], + }, + HT_OVER_UNDER: { + period: 'HT', + lineValue: 1.5, + allowParlay: true, + sortOrder: 7, + selections: [ + { code: 'OVER', name: 'HT Over 1.5', odds: 2.0 }, + { code: 'UNDER', name: 'HT Under 1.5', odds: 1.75 }, + ], + }, + FT_ODD_EVEN: { + period: 'FT', + allowParlay: true, + sortOrder: 4, + selections: [ + { code: 'ODD', name: 'Odd', odds: 1.9 }, + { code: 'EVEN', name: 'Even', odds: 1.9 }, + ], + }, + FT_CORRECT_SCORE: { + period: 'FT', + allowParlay: true, + sortOrder: 8, + selections: FT_CORRECT_SCORE_TEMPLATE.map((code) => ({ + code, + name: code.replace('SCORE_', '').replace('_', '-') || code, + odds: 8.0, + })), + }, + HT_CORRECT_SCORE: { + period: 'HT', + allowParlay: true, + sortOrder: 9, + selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({ + code, + name: code.replace('SCORE_', '').replace('_', '-') || code, + odds: 6.0, + })), + }, + SH_CORRECT_SCORE: { + period: 'SH', + allowParlay: true, + sortOrder: 10, + selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({ + code, + name: code.replace('SCORE_', '').replace('_', '-') || code, + odds: 6.0, + })), + }, + }; + + const config = configs[marketType]; + if (!config) throw new BadRequestException(`Unknown market type: ${marketType}`); + return config; + } + + async updateOdds(selectionId: bigint, newOdds: number, operatorId: bigint) { + const selection = await this.prisma.marketSelection.findUnique({ + where: { id: selectionId }, + }); + if (!selection) throw new NotFoundException('Selection not found'); + if (newOdds <= 1) throw new BadRequestException('Odds must be > 1.00'); + + const newVersion = selection.oddsVersion + BigInt(1); + + return this.prisma.$transaction(async (tx) => { + await tx.oddsChangeLog.create({ + data: { + selectionId, + oldOdds: selection.odds, + newOdds, + oddsVersion: newVersion, + changedBy: operatorId, + }, + }); + + return tx.marketSelection.update({ + where: { id: selectionId }, + data: { odds: newOdds, oddsVersion: newVersion }, + }); + }); + } + + async batchUpdateOdds( + updates: Array<{ selectionId: bigint; odds: number }>, + operatorId: bigint, + ) { + const results = []; + for (const u of updates) { + results.push(await this.updateOdds(u.selectionId, u.odds, operatorId)); + } + return results; + } +} diff --git a/apps/api/src/matches/matches.module.ts b/apps/api/src/matches/matches.module.ts new file mode 100644 index 0000000..68d9672 --- /dev/null +++ b/apps/api/src/matches/matches.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { MatchesService } from './matches.service'; + +@Module({ + providers: [MatchesService], + exports: [MatchesService], +}) +export class MatchesModule {} diff --git a/apps/api/src/matches/matches.service.ts b/apps/api/src/matches/matches.service.ts new file mode 100644 index 0000000..97d444c --- /dev/null +++ b/apps/api/src/matches/matches.service.ts @@ -0,0 +1,161 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class MatchesService { + constructor(private prisma: PrismaService) {} + + async createLeague(code: string, translations: Record) { + const league = await this.prisma.league.create({ data: { code } }); + for (const [locale, value] of Object.entries(translations)) { + await this.prisma.entityTranslation.create({ + data: { + entityType: 'LEAGUE', + entityId: league.id, + locale, + fieldName: 'name', + value, + }, + }); + } + return league; + } + + async createTeam(code: string, translations: Record) { + const team = await this.prisma.team.create({ data: { code } }); + for (const [locale, value] of Object.entries(translations)) { + await this.prisma.entityTranslation.create({ + data: { + entityType: 'TEAM', + entityId: team.id, + locale, + fieldName: 'name', + value, + }, + }); + } + return team; + } + + async createMatch(data: { + leagueId: bigint; + homeTeamId: bigint; + awayTeamId: bigint; + startTime: Date; + isHot?: boolean; + createdBy?: bigint; + }) { + return this.prisma.match.create({ + data: { + leagueId: data.leagueId, + homeTeamId: data.homeTeamId, + awayTeamId: data.awayTeamId, + startTime: data.startTime, + isHot: data.isHot ?? false, + createdBy: data.createdBy, + status: 'DRAFT', + }, + }); + } + + async publishMatch(matchId: bigint) { + return this.prisma.match.update({ + where: { id: matchId }, + data: { status: 'PUBLISHED', publishTime: new Date() }, + }); + } + + async closeMatch(matchId: bigint) { + return this.prisma.match.update({ + where: { id: matchId }, + data: { status: 'CLOSED', closeTime: new Date() }, + }); + } + + async cancelMatch(matchId: bigint) { + return this.prisma.match.update({ + where: { id: matchId }, + data: { status: 'CANCELLED' }, + }); + } + + async getTranslation(entityType: string, entityId: bigint, locale: string) { + const translations = await this.prisma.entityTranslation.findMany({ + where: { entityType, entityId }, + }); + const map = Object.fromEntries( + translations.filter((t) => t.fieldName === 'name').map((t) => [t.locale, t.value]), + ); + return map[locale] || map['en-US'] || map['zh-CN'] || Object.values(map)[0] || ''; + } + + async enrichMatch(match: Record, locale: string) { + const m = match as { + id: bigint; + leagueId: bigint; + homeTeamId: bigint; + awayTeamId: bigint; + league?: unknown; + homeTeam?: unknown; + awayTeam?: unknown; + markets?: unknown[]; + }; + const [leagueName, homeName, awayName] = await Promise.all([ + this.getTranslation('LEAGUE', m.leagueId, locale), + this.getTranslation('TEAM', m.homeTeamId, locale), + this.getTranslation('TEAM', m.awayTeamId, locale), + ]); + return { + ...match, + leagueName, + homeTeamName: homeName, + awayTeamName: awayName, + }; + } + + async listPublished(locale = 'en-US', leagueId?: bigint) { + const matches = await this.prisma.match.findMany({ + where: { + status: 'PUBLISHED', + ...(leagueId ? { leagueId } : {}), + }, + include: { + markets: { + where: { status: 'OPEN' }, + include: { selections: { where: { status: 'OPEN' } } }, + }, + }, + orderBy: [{ isHot: 'desc' }, { startTime: 'asc' }], + }); + + return Promise.all(matches.map((m) => this.enrichMatch(m, locale))); + } + + async getMatchDetail(matchId: bigint, locale = 'en-US') { + const match = await this.prisma.match.findUnique({ + where: { id: matchId }, + include: { + markets: { + include: { selections: true }, + orderBy: { sortOrder: 'asc' }, + }, + score: true, + }, + }); + if (!match) throw new NotFoundException('Match not found'); + return this.enrichMatch(match, locale); + } + + @Cron(CronExpression.EVERY_MINUTE) + async autoCloseMatches() { + const now = new Date(); + await this.prisma.match.updateMany({ + where: { + status: 'PUBLISHED', + startTime: { lte: now }, + }, + data: { status: 'CLOSED', closeTime: now }, + }); + } +} diff --git a/apps/api/src/player/player.controller.ts b/apps/api/src/player/player.controller.ts new file mode 100644 index 0000000..812dc04 --- /dev/null +++ b/apps/api/src/player/player.controller.ts @@ -0,0 +1,179 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Query, + UseGuards, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard, PlayerGuard } from '../auth/guards'; +import { CurrentUser } from '../common/decorators'; +import { jsonResponse } from '../common/filters'; +import { UsersService } from '../users/users.service'; +import { WalletService } from '../wallet/wallet.service'; +import { MatchesService } from '../matches/matches.service'; +import { BetsService } from '../bets/bets.service'; +import { ContentService } from '../content/content.service'; +import { CashbackService } from '../cashback/cashback.service'; +import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + +class SingleBetDto { + @IsString() + selectionId!: string; + + @IsString() + oddsVersion!: string; + + @IsNumber() + @Min(0.01) + stake!: number; + + @IsString() + requestId!: string; +} + +class ParlayLegDto { + @IsString() + selectionId!: string; + + @IsString() + oddsVersion!: string; +} + +class ParlayBetDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ParlayLegDto) + legs!: ParlayLegDto[]; + + @IsNumber() + @Min(0.01) + stake!: number; + + @IsString() + requestId!: string; +} + +class LocaleDto { + @IsString() + locale!: string; +} + +@ApiTags('Player') +@Controller('player') +@UseGuards(JwtAuthGuard, PlayerGuard) +@ApiBearerAuth() +export class PlayerController { + constructor( + private users: UsersService, + private wallet: WalletService, + private matches: MatchesService, + private bets: BetsService, + private content: ContentService, + private cashback: CashbackService, + ) {} + + @Get('profile') + async profile(@CurrentUser('id') userId: bigint) { + const user = await this.users.findById(userId); + return jsonResponse(user); + } + + @Post('language') + async setLanguage(@CurrentUser('id') userId: bigint, @Body() dto: LocaleDto) { + const result = await this.users.updateLocale(userId, dto.locale); + return jsonResponse(result); + } + + @Get('home') + async home(@CurrentUser('locale') locale: string) { + const [banners, notices, ticker, hotMatches, todayMatches] = await Promise.all([ + this.content.listActive('BANNER', locale), + this.content.listActive('NOTICE', locale), + this.content.listActive('TICKER', locale), + this.matches.listPublished(locale), + this.matches.listPublished(locale), + ]); + return jsonResponse({ + banners, + notices, + ticker, + hotMatches: (hotMatches as Array<{ isHot?: boolean }>).filter((m) => m.isHot), + todayMatches, + }); + } + + @Get('matches') + async listMatches( + @CurrentUser('locale') locale: string, + @Query('leagueId') leagueId?: string, + ) { + const items = await this.matches.listPublished(locale, leagueId ? BigInt(leagueId) : undefined); + return jsonResponse(items); + } + + @Get('matches/:id') + async matchDetail(@Param('id') id: string, @CurrentUser('locale') locale: string) { + const match = await this.matches.getMatchDetail(BigInt(id), locale); + return jsonResponse(match); + } + + @Post('bets/single') + async singleBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: SingleBetDto) { + const bet = await this.bets.placeSingleBet( + userId, + parentId, + BigInt(dto.selectionId), + BigInt(dto.oddsVersion), + dto.stake, + dto.requestId, + ); + return jsonResponse(bet); + } + + @Post('bets/parlay') + async parlayBet(@CurrentUser('id') userId: bigint, @CurrentUser('parentId') parentId: bigint, @Body() dto: ParlayBetDto) { + const bet = await this.bets.placeParlayBet( + userId, + parentId, + dto.legs.map((l) => ({ selectionId: BigInt(l.selectionId), oddsVersion: BigInt(l.oddsVersion) })), + dto.stake, + dto.requestId, + ); + return jsonResponse(bet); + } + + @Get('bets') + async myBets( + @CurrentUser('id') userId: bigint, + @Query('status') status?: string, + @Query('page') page?: string, + ) { + const result = await this.bets.getUserBets(userId, status, page ? parseInt(page) : 1); + return jsonResponse(result); + } + + @Get('bets/:betNo') + async betDetail(@CurrentUser('id') userId: bigint, @Param('betNo') betNo: string) { + const bet = await this.bets.getBetByNo(betNo, userId); + return jsonResponse(bet); + } + + @Get('wallet/transactions') + async transactions( + @CurrentUser('id') userId: bigint, + @Query('page') page?: string, + ) { + const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1); + return jsonResponse(result); + } + + @Get('cashbacks') + async cashbacks(@CurrentUser('id') userId: bigint) { + const items = await this.cashback.getUserCashbacks(userId); + return jsonResponse(items); + } +} diff --git a/apps/api/src/player/player.module.ts b/apps/api/src/player/player.module.ts new file mode 100644 index 0000000..9cb8f27 --- /dev/null +++ b/apps/api/src/player/player.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { PlayerController } from './player.controller'; +import { UsersModule } from '../users/users.module'; +import { WalletModule } from '../wallet/wallet.module'; +import { MatchesModule } from '../matches/matches.module'; +import { BetsModule } from '../bets/bets.module'; +import { ContentModule } from '../content/content.module'; +import { CashbackModule } from '../cashback/cashback.module'; + +@Module({ + imports: [UsersModule, WalletModule, MatchesModule, BetsModule, ContentModule, CashbackModule], + controllers: [PlayerController], +}) +export class PlayerModule {} diff --git a/apps/api/src/prisma/prisma.module.ts b/apps/api/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/apps/api/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts new file mode 100644 index 0000000..bb6565f --- /dev/null +++ b/apps/api/src/prisma/prisma.service.ts @@ -0,0 +1,13 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/apps/api/src/settlement/settlement-calculator.spec.ts b/apps/api/src/settlement/settlement-calculator.spec.ts new file mode 100644 index 0000000..6a9fe41 --- /dev/null +++ b/apps/api/src/settlement/settlement-calculator.spec.ts @@ -0,0 +1,187 @@ +import { + settleSelection, + calculatePayout, + calculateParlayPayout, + isQuarterHandicapOrTotal, + ScoreInput, +} from './settlement-calculator'; + +describe('SettlementCalculator', () => { + const score: ScoreInput = { htHome: 1, htAway: 0, ftHome: 2, ftAway: 1 }; + + describe('FT_1X2', () => { + it('S001: home win', () => { + expect( + settleSelection({ marketType: 'FT_1X2', selectionCode: 'HOME', score }), + ).toBe('WIN'); + expect( + settleSelection({ marketType: 'FT_1X2', selectionCode: 'DRAW', score }), + ).toBe('LOSE'); + }); + + it('S002: draw', () => { + const draw = { htHome: 0, htAway: 0, ftHome: 1, ftAway: 1 }; + expect( + settleSelection({ marketType: 'FT_1X2', selectionCode: 'DRAW', score: draw }), + ).toBe('WIN'); + }); + }); + + describe('HT_1X2', () => { + it('S003: half time result', () => { + expect( + settleSelection({ marketType: 'HT_1X2', selectionCode: 'HOME', score }), + ).toBe('WIN'); + }); + }); + + describe('FT_ODD_EVEN', () => { + it('S004: 0-0 is even', () => { + const s = { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 }; + expect( + settleSelection({ marketType: 'FT_ODD_EVEN', selectionCode: 'EVEN', score: s }), + ).toBe('WIN'); + }); + }); + + describe('Correct Score', () => { + it('S005: exact score 2-1', () => { + expect( + settleSelection({ + marketType: 'FT_CORRECT_SCORE', + selectionCode: 'SCORE_2_1', + score, + }), + ).toBe('WIN'); + }); + + it('S006: other home win', () => { + const s = { htHome: 2, htAway: 0, ftHome: 5, ftAway: 0 }; + expect( + settleSelection({ + marketType: 'FT_CORRECT_SCORE', + selectionCode: 'OTHER_HOME', + score: s, + templateScores: ['SCORE_1_0', 'SCORE_2_0'], + }), + ).toBe('WIN'); + }); + + it('S008: second half correct score', () => { + expect( + settleSelection({ + marketType: 'SH_CORRECT_SCORE', + selectionCode: 'SCORE_1_1', + score, + }), + ).toBe('WIN'); + }); + }); + + describe('Handicap', () => { + it('S009: full win', () => { + const r = settleSelection({ + marketType: 'FT_HANDICAP', + selectionCode: 'HOME', + handicapLine: -1, + score: { htHome: 0, htAway: 0, ftHome: 2, ftAway: 0 }, + }); + expect(r).toBe('WIN'); + expect(calculatePayout(100, 1.85, r).toNumber()).toBe(185); + }); + + it('S010: push', () => { + const r = settleSelection({ + marketType: 'FT_HANDICAP', + selectionCode: 'HOME', + handicapLine: -1, + score: { htHome: 0, htAway: 0, ftHome: 1, ftAway: 0 }, + }); + expect(r).toBe('PUSH'); + expect(calculatePayout(100, 1.85, r).toNumber()).toBe(100); + }); + + it('S011: half win -0.25', () => { + const r = settleSelection({ + marketType: 'FT_HANDICAP', + selectionCode: 'HOME', + handicapLine: -0.25, + score: { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 }, + }); + expect(r).toBe('HALF_LOSE'); + }); + }); + + describe('Over/Under', () => { + it('S013: over 2.5 wins with 3 goals', () => { + const s = { htHome: 1, htAway: 1, ftHome: 2, ftAway: 1 }; + expect( + settleSelection({ + marketType: 'FT_OVER_UNDER', + selectionCode: 'OVER', + totalLine: 2.5, + score: s, + }), + ).toBe('WIN'); + }); + + it('S014: push on integer line', () => { + const s = { htHome: 1, htAway: 0, ftHome: 1, ftAway: 1 }; + expect( + settleSelection({ + marketType: 'FT_OVER_UNDER', + selectionCode: 'OVER', + totalLine: 2, + score: s, + }), + ).toBe('PUSH'); + }); + }); + + describe('Parlay', () => { + it('S016: all win', () => { + const result = calculateParlayPayout(100, [ + { odds: 1.8, result: 'WIN' }, + { odds: 2.0, result: 'WIN' }, + ]); + expect(result.betResult).toBe('WON'); + expect(result.payout.toNumber()).toBe(360); + }); + + it('S017: one lose', () => { + const result = calculateParlayPayout(100, [ + { odds: 1.8, result: 'WIN' }, + { odds: 2.0, result: 'LOSE' }, + ]); + expect(result.betResult).toBe('LOST'); + expect(result.payout.toNumber()).toBe(0); + }); + + it('S018: one push', () => { + const result = calculateParlayPayout(100, [ + { odds: 1.8, result: 'WIN' }, + { odds: 2.0, result: 'PUSH' }, + { odds: 1.9, result: 'WIN' }, + ]); + expect(result.betResult).toBe('WON'); + expect(result.payout.toNumber()).toBe(342); + }); + + it('S019: all push', () => { + const result = calculateParlayPayout(100, [ + { odds: 1.8, result: 'PUSH' }, + { odds: 2.0, result: 'VOID' }, + ]); + expect(result.betResult).toBe('PUSH'); + expect(result.payout.toNumber()).toBe(100); + }); + }); + + describe('Quarter line detection', () => { + it('detects quarter lines', () => { + expect(isQuarterHandicapOrTotal(-0.25)).toBe(true); + expect(isQuarterHandicapOrTotal(2.5)).toBe(false); + expect(isQuarterHandicapOrTotal(-1)).toBe(false); + }); + }); +}); diff --git a/apps/api/src/settlement/settlement-calculator.ts b/apps/api/src/settlement/settlement-calculator.ts new file mode 100644 index 0000000..d168fb5 --- /dev/null +++ b/apps/api/src/settlement/settlement-calculator.ts @@ -0,0 +1,279 @@ +import Decimal from 'decimal.js'; + +export type SelectionResult = 'WIN' | 'HALF_WIN' | 'PUSH' | 'HALF_LOSE' | 'LOSE' | 'VOID'; + +export interface ScoreInput { + htHome: number; + htAway: number; + ftHome: number; + ftAway: number; +} + +export interface SettlementInput { + marketType: string; + selectionCode: string; + handicapLine?: number | null; + totalLine?: number | null; + score: ScoreInput; + templateScores?: string[]; +} + +export function getShScore(score: ScoreInput): { home: number; away: number } { + return { + home: score.ftHome - score.htHome, + away: score.ftAway - score.htAway, + }; +} + +function isQuarterLine(line: number): boolean { + const frac = Math.abs(line % 1); + return Math.abs(frac - 0.25) < 0.001 || Math.abs(frac - 0.75) < 0.001; +} + +function splitQuarterLine(line: number): [number, number] { + const sign = line >= 0 ? 1 : -1; + const abs = Math.abs(line); + const lower = Math.floor(abs * 2) / 2 * sign; + const upper = (Math.floor(abs * 2) + 1) / 2 * sign; + if (Math.abs(abs % 1 - 0.25) < 0.001) { + return [lower, upper]; + } + // 0.75 case: e.g. -0.75 => -0.5 and -1 + const l = Math.floor(abs) * sign; + const u = (Math.floor(abs) + 0.5) * sign; + return abs % 1 > 0.5 ? [l, u] : [lower, upper]; +} + +function settleHandicap( + teamGoals: number, + oppGoals: number, + handicap: number, + isHome: boolean, +): SelectionResult { + const adj = teamGoals + handicap - oppGoals; + + if (!isQuarterLine(handicap)) { + if (adj > 0) return 'WIN'; + if (adj === 0) return 'PUSH'; + return 'LOSE'; + } + + const [line1, line2] = splitQuarterLine(handicap); + const r1 = teamGoals + line1 - oppGoals; + const r2 = teamGoals + line2 - oppGoals; + + const results: SelectionResult[] = []; + for (const r of [r1, r2]) { + if (r > 0) results.push('WIN'); + else if (r === 0) results.push('PUSH'); + else results.push('LOSE'); + } + + const winCount = results.filter((r) => r === 'WIN').length; + const loseCount = results.filter((r) => r === 'LOSE').length; + + if (winCount === 2) return 'WIN'; + if (loseCount === 2) return 'LOSE'; + if (winCount === 1 && loseCount === 0) return 'HALF_WIN'; + if (loseCount === 1 && winCount === 0) return 'HALF_LOSE'; + return 'PUSH'; +} + +function settleOverUnder( + totalGoals: number, + line: number, + isOver: boolean, +): SelectionResult { + if (!isQuarterLine(line)) { + if (isOver) { + if (totalGoals > line) return 'WIN'; + if (totalGoals === line) return 'PUSH'; + return 'LOSE'; + } + if (totalGoals < line) return 'WIN'; + if (totalGoals === line) return 'PUSH'; + return 'LOSE'; + } + + const [line1, line2] = splitQuarterLine(line); + const r1 = settleOverUnder(totalGoals, line1, isOver); + const r2 = settleOverUnder(totalGoals, line2, isOver); + + const winCount = [r1, r2].filter((r) => r === 'WIN').length; + const loseCount = [r1, r2].filter((r) => r === 'LOSE').length; + + if (winCount === 2) return 'WIN'; + if (loseCount === 2) return 'LOSE'; + if (winCount === 1) return 'HALF_WIN'; + if (loseCount === 1) return 'HALF_LOSE'; + return 'PUSH'; +} + +function parseScoreCode(code: string): { home: number; away: number } | null { + const match = code.match(/SCORE_(\d+)_(\d+)/); + if (match) return { home: parseInt(match[1]), away: parseInt(match[2]) }; + return null; +} + +function settleCorrectScore( + home: number, + away: number, + selectionCode: string, + templateScores: string[], +): SelectionResult { + const parsed = parseScoreCode(selectionCode); + if (parsed) { + return parsed.home === home && parsed.away === away ? 'WIN' : 'LOSE'; + } + + const actualKey = `SCORE_${home}_${away}`; + if (templateScores.includes(actualKey)) { + return 'LOSE'; + } + + const isDraw = home === away; + const isHomeWin = home > away; + + if (selectionCode === 'OTHER_DRAW' && isDraw) return 'WIN'; + if (selectionCode === 'OTHER_HOME' && isHomeWin) return 'WIN'; + if (selectionCode === 'OTHER_AWAY' && !isHomeWin && !isDraw) return 'WIN'; + + return 'LOSE'; +} + +export function settleSelection(input: SettlementInput): SelectionResult { + const { marketType, selectionCode, handicapLine, totalLine, score } = input; + const templates = input.templateScores ?? []; + + switch (marketType) { + case 'FT_1X2': { + if (selectionCode === 'HOME') return score.ftHome > score.ftAway ? 'WIN' : 'LOSE'; + if (selectionCode === 'DRAW') return score.ftHome === score.ftAway ? 'WIN' : 'LOSE'; + if (selectionCode === 'AWAY') return score.ftHome < score.ftAway ? 'WIN' : 'LOSE'; + break; + } + case 'HT_1X2': { + if (selectionCode === 'HOME') return score.htHome > score.htAway ? 'WIN' : 'LOSE'; + if (selectionCode === 'DRAW') return score.htHome === score.htAway ? 'WIN' : 'LOSE'; + if (selectionCode === 'AWAY') return score.htHome < score.htAway ? 'WIN' : 'LOSE'; + break; + } + case 'FT_ODD_EVEN': { + const total = score.ftHome + score.ftAway; + const isOdd = total % 2 === 1; + if (selectionCode === 'ODD') return isOdd ? 'WIN' : 'LOSE'; + if (selectionCode === 'EVEN') return !isOdd ? 'WIN' : 'LOSE'; + break; + } + case 'FT_HANDICAP': { + const line = handicapLine ?? 0; + const isHome = selectionCode === 'HOME'; + const goals = isHome ? score.ftHome : score.ftAway; + const opp = isHome ? score.ftAway : score.ftHome; + return settleHandicap(goals, opp, isHome ? line : -line, isHome); + } + case 'HT_HANDICAP': { + const line = handicapLine ?? 0; + const isHome = selectionCode === 'HOME'; + const goals = isHome ? score.htHome : score.htAway; + const opp = isHome ? score.htAway : score.htHome; + return settleHandicap(goals, opp, isHome ? line : -line, isHome); + } + case 'FT_OVER_UNDER': { + const total = score.ftHome + score.ftAway; + return settleOverUnder(total, totalLine ?? 0, selectionCode === 'OVER'); + } + case 'HT_OVER_UNDER': { + const total = score.htHome + score.htAway; + return settleOverUnder(total, totalLine ?? 0, selectionCode === 'OVER'); + } + case 'FT_CORRECT_SCORE': + return settleCorrectScore(score.ftHome, score.ftAway, selectionCode, templates); + case 'HT_CORRECT_SCORE': + return settleCorrectScore(score.htHome, score.htAway, selectionCode, templates); + case 'SH_CORRECT_SCORE': { + const sh = getShScore(score); + return settleCorrectScore(sh.home, sh.away, selectionCode, templates); + } + case 'OUTRIGHT_WINNER': + return selectionCode === `TEAM_${input.score.ftHome}` ? 'WIN' : 'LOSE'; + } + + return 'LOSE'; +} + +export function calculatePayout( + stake: Decimal | number, + odds: Decimal | number, + result: SelectionResult, +): Decimal { + const s = new Decimal(stake); + const o = new Decimal(odds); + + switch (result) { + case 'WIN': + return s.mul(o); + case 'HALF_WIN': + return s.div(2).mul(o).add(s.div(2)); + case 'PUSH': + case 'VOID': + return s; + case 'HALF_LOSE': + return s.div(2); + case 'LOSE': + return new Decimal(0); + default: + return new Decimal(0); + } +} + +export function calculateParlayPayout( + stake: Decimal | number, + selections: Array<{ odds: Decimal | number; result: SelectionResult }>, +): { betResult: 'WON' | 'LOST' | 'PUSH'; payout: Decimal; effectiveOdds: Decimal } { + const s = new Decimal(stake); + + if (selections.some((sel) => sel.result === 'LOSE')) { + return { betResult: 'LOST', payout: new Decimal(0), effectiveOdds: new Decimal(0) }; + } + + let combinedOdds = new Decimal(1); + for (const sel of selections) { + if (sel.result === 'WIN') { + combinedOdds = combinedOdds.mul(sel.odds); + } else if (sel.result === 'HALF_WIN') { + combinedOdds = combinedOdds.mul(new Decimal(sel.odds).add(1).div(2)); + } else if (sel.result === 'HALF_LOSE') { + combinedOdds = combinedOdds.mul(0.5); + } + // PUSH/VOID => odds 1.00 + } + + const allPush = selections.every( + (sel) => sel.result === 'PUSH' || sel.result === 'VOID', + ); + if (allPush) { + return { betResult: 'PUSH', payout: s, effectiveOdds: new Decimal(1) }; + } + + return { betResult: 'WON', payout: s.mul(combinedOdds), effectiveOdds: combinedOdds }; +} + +export function isQuarterHandicapOrTotal(line: number | null | undefined): boolean { + if (line == null) return false; + return isQuarterLine(line); +} + +export const FT_CORRECT_SCORE_TEMPLATE = [ + 'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'SCORE_3_3', 'SCORE_4_4', 'OTHER_DRAW', + 'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'SCORE_3_1', 'SCORE_3_2', + 'SCORE_4_0', 'SCORE_4_1', 'SCORE_4_2', 'SCORE_4_3', 'OTHER_HOME', + 'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'SCORE_1_3', 'SCORE_2_3', + 'SCORE_0_4', 'SCORE_1_4', 'SCORE_2_4', 'SCORE_3_4', 'OTHER_AWAY', +]; + +export const HT_CORRECT_SCORE_TEMPLATE = [ + 'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'OTHER_DRAW', + 'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'OTHER_HOME', + 'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'OTHER_AWAY', +]; diff --git a/apps/api/src/settlement/settlement.module.ts b/apps/api/src/settlement/settlement.module.ts new file mode 100644 index 0000000..d8be42a --- /dev/null +++ b/apps/api/src/settlement/settlement.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SettlementService } from './settlement.service'; +import { WalletModule } from '../wallet/wallet.module'; +import { AgentsModule } from '../agents/agents.module'; + +@Module({ + imports: [WalletModule, AgentsModule], + providers: [SettlementService], + exports: [SettlementService], +}) +export class SettlementModule {} diff --git a/apps/api/src/settlement/settlement.service.ts b/apps/api/src/settlement/settlement.service.ts new file mode 100644 index 0000000..3ab13d2 --- /dev/null +++ b/apps/api/src/settlement/settlement.service.ts @@ -0,0 +1,312 @@ +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { WalletService } from '../wallet/wallet.service'; +import { AgentsService } from '../agents/agents.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { generateBatchNo } from '../common/decorators'; +import { + settleSelection, + calculatePayout, + calculateParlayPayout, + ScoreInput, + FT_CORRECT_SCORE_TEMPLATE, + HT_CORRECT_SCORE_TEMPLATE, +} from './settlement-calculator'; + +@Injectable() +export class SettlementService { + constructor( + private prisma: PrismaService, + private wallet: WalletService, + private agents: AgentsService, + ) {} + + async recordScore( + matchId: bigint, + htHome: number, + htAway: number, + ftHome: number, + ftAway: number, + operatorId: bigint, + ) { + await this.prisma.matchScore.upsert({ + where: { matchId }, + create: { matchId, htHomeScore: htHome, htAwayScore: htAway, ftHomeScore: ftHome, ftAwayScore: ftAway, recordedBy: operatorId }, + update: { htHomeScore: htHome, htAwayScore: htAway, ftHomeScore: ftHome, ftAwayScore: ftAway, recordedBy: operatorId }, + }); + + await this.prisma.match.update({ + where: { id: matchId }, + data: { status: 'PENDING_SETTLEMENT' }, + }); + + return { matchId, htHome, htAway, ftHome, ftAway }; + } + + async previewSettlement(matchId: bigint, operatorId: bigint) { + const score = await this.prisma.matchScore.findUnique({ where: { matchId } }); + if (!score) throw new BadRequestException('Score not recorded'); + + const scoreInput: ScoreInput = { + htHome: score.htHomeScore ?? 0, + htAway: score.htAwayScore ?? 0, + ftHome: score.ftHomeScore ?? 0, + ftAway: score.ftAwayScore ?? 0, + }; + + const pendingBets = await this.prisma.bet.findMany({ + where: { + status: 'PENDING', + selections: { some: { matchId } }, + }, + include: { selections: true }, + }); + + const parlayBets = await this.prisma.bet.findMany({ + where: { + status: 'PENDING', + betType: 'PARLAY', + selections: { some: { matchId } }, + }, + include: { selections: true }, + }); + + let totalPayout = new Decimal(0); + let totalRefund = new Decimal(0); + const items: Array<{ betId: bigint; betNo: string; result: string; payout: Decimal }> = []; + + for (const bet of pendingBets) { + if (bet.betType === 'SINGLE') { + const sel = bet.selections[0]; + const template = + sel.marketType === 'FT_CORRECT_SCORE' + ? FT_CORRECT_SCORE_TEMPLATE + : sel.marketType.includes('CORRECT_SCORE') + ? HT_CORRECT_SCORE_TEMPLATE + : []; + + const result = settleSelection({ + marketType: sel.marketType, + selectionCode: sel.selectionNameSnapshot.includes('-') + ? `SCORE_${sel.selectionNameSnapshot.replace('-', '_')}` + : sel.selectionNameSnapshot, + handicapLine: sel.handicapLine ? Number(sel.handicapLine) : null, + totalLine: sel.totalLine ? Number(sel.totalLine) : null, + score: scoreInput, + templateScores: template, + }); + + const payout = calculatePayout(bet.stake, sel.odds, result); + items.push({ betId: bet.id, betNo: bet.betNo, result, payout }); + + if (result === 'LOSE') { + // no payout + } else if (result === 'PUSH' || result === 'VOID') { + totalRefund = totalRefund.add(bet.stake); + } else { + totalPayout = totalPayout.add(payout); + } + } + } + + const batch = await this.prisma.settlementBatch.create({ + data: { + matchId, + batchNo: generateBatchNo('STL'), + htHomeScore: score.htHomeScore, + htAwayScore: score.htAwayScore, + ftHomeScore: score.ftHomeScore, + ftAwayScore: score.ftAwayScore, + status: 'PREVIEW', + totalBets: pendingBets.length, + totalPayout, + totalRefund, + operatorId, + }, + }); + + return { + batch, + score: scoreInput, + singleBetCount: pendingBets.filter((b) => b.betType === 'SINGLE').length, + parlayBetCount: parlayBets.length, + items, + totalPayout, + totalRefund, + }; + } + + async confirmSettlement(batchId: bigint, operatorId: bigint) { + const batch = await this.prisma.settlementBatch.findUnique({ + where: { id: batchId }, + include: { match: true }, + }); + if (!batch) throw new NotFoundException('Batch not found'); + if (batch.status !== 'PREVIEW') throw new BadRequestException('Batch already confirmed'); + + const score = await this.prisma.matchScore.findUnique({ + where: { matchId: batch.matchId }, + }); + if (!score) throw new BadRequestException('Score not found'); + + const scoreInput: ScoreInput = { + htHome: score.htHomeScore ?? 0, + htAway: score.htAwayScore ?? 0, + ftHome: score.ftHomeScore ?? 0, + ftAway: score.ftAwayScore ?? 0, + }; + + const pendingBets = await this.prisma.bet.findMany({ + where: { + status: 'PENDING', + selections: { some: { matchId: batch.matchId } }, + }, + include: { selections: true, user: true }, + }); + + const agentIds = new Set(); + + await this.prisma.$transaction(async (tx) => { + for (const bet of pendingBets) { + if (bet.betType === 'SINGLE') { + const sel = bet.selections[0]; + const selection = await tx.marketSelection.findUnique({ + where: { id: sel.selectionId }, + }); + + const result = settleSelection({ + marketType: sel.marketType, + selectionCode: selection?.selectionCode ?? sel.selectionNameSnapshot, + handicapLine: sel.handicapLine ? Number(sel.handicapLine) : null, + totalLine: sel.totalLine ? Number(sel.totalLine) : null, + score: scoreInput, + templateScores: + sel.marketType === 'FT_CORRECT_SCORE' + ? FT_CORRECT_SCORE_TEMPLATE + : HT_CORRECT_SCORE_TEMPLATE, + }); + + const payout = calculatePayout(bet.stake, sel.odds, result); + const betStatus = + result === 'LOSE' ? 'LOST' : result === 'PUSH' || result === 'VOID' ? 'PUSH' : 'WON'; + + await tx.bet.update({ + where: { id: bet.id }, + data: { + status: betStatus, + actualReturn: payout, + settledAt: new Date(), + }, + }); + + await tx.betSelection.update({ + where: { id: sel.id }, + data: { resultStatus: result, effectiveOdds: sel.odds }, + }); + + await this.wallet.settleBet( + bet.userId, + bet.stake, + payout, + bet.betNo, + result === 'HALF_WIN' ? 'HALF_WIN' : result === 'HALF_LOSE' ? 'HALF_LOSE' : result as 'WIN' | 'LOSE' | 'PUSH' | 'VOID', + ); + + if (bet.agentId) agentIds.add(bet.agentId); + + await tx.settlementItem.create({ + data: { + batchId, + betId: bet.id, + userId: bet.userId, + result: betStatus, + payout, + }, + }); + } else { + // Parlay: update this leg's result, check if all legs settled + for (const sel of bet.selections) { + if (sel.matchId?.toString() === batch.matchId.toString()) { + const selection = await tx.marketSelection.findUnique({ + where: { id: sel.selectionId }, + }); + const result = settleSelection({ + marketType: sel.marketType, + selectionCode: selection?.selectionCode ?? '', + handicapLine: sel.handicapLine ? Number(sel.handicapLine) : null, + totalLine: sel.totalLine ? Number(sel.totalLine) : null, + score: scoreInput, + }); + await tx.betSelection.update({ + where: { id: sel.id }, + data: { resultStatus: result }, + }); + } + } + + const updated = await tx.betSelection.findMany({ where: { betId: bet.id } }); + const allHaveResult = updated.every((s) => s.resultStatus != null); + + if (allHaveResult) { + const legResults = updated.map((s) => ({ + odds: s.odds, + result: s.resultStatus as 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE', + })); + const parlayResult = calculateParlayPayout(bet.stake, legResults); + + await tx.bet.update({ + where: { id: bet.id }, + data: { + status: parlayResult.betResult === 'LOST' ? 'LOST' : parlayResult.betResult === 'PUSH' ? 'PUSH' : 'WON', + actualReturn: parlayResult.payout, + settledAt: new Date(), + }, + }); + + await this.wallet.settleBet( + bet.userId, + bet.stake, + parlayResult.payout, + bet.betNo, + parlayResult.betResult === 'LOST' ? 'LOSE' : parlayResult.betResult === 'PUSH' ? 'PUSH' : 'WIN', + ); + + if (bet.agentId) agentIds.add(bet.agentId); + } + } + } + + await tx.settlementBatch.update({ + where: { id: batchId }, + data: { status: 'CONFIRMED', confirmedAt: new Date() }, + }); + + await tx.match.update({ + where: { id: batch.matchId }, + data: { status: 'SETTLED' }, + }); + }); + + for (const agentId of agentIds) { + await this.agents.recalculateUsedCredit(agentId); + } + + return { success: true, batchId: batchId.toString() }; + } + + async voidMatchBets(matchId: bigint) { + const bets = await this.prisma.bet.findMany({ + where: { status: 'PENDING', selections: { some: { matchId } } }, + }); + + for (const bet of bets) { + await this.wallet.settleBet(bet.userId, bet.stake, bet.stake, bet.betNo, 'VOID'); + await this.prisma.bet.update({ + where: { id: bet.id }, + data: { status: 'VOID', actualReturn: bet.stake, settledAt: new Date() }, + }); + } + + return { voidedCount: bets.length }; + } +} diff --git a/apps/api/src/users/users.module.ts b/apps/api/src/users/users.module.ts new file mode 100644 index 0000000..8fa904f --- /dev/null +++ b/apps/api/src/users/users.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { UsersService } from './users.service'; + +@Module({ + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/apps/api/src/users/users.service.ts b/apps/api/src/users/users.service.ts new file mode 100644 index 0000000..9bd7749 --- /dev/null +++ b/apps/api/src/users/users.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class UsersService { + constructor(private prisma: PrismaService) {} + + async findById(id: bigint) { + return this.prisma.user.findUnique({ + where: { id }, + include: { wallet: true, agentProfile: true, preferences: true }, + }); + } + + async updateLocale(userId: bigint, locale: string) { + await this.prisma.user.update({ + where: { id: userId }, + data: { locale }, + }); + await this.prisma.userPreference.upsert({ + where: { userId }, + create: { userId, locale }, + update: { locale }, + }); + return { locale }; + } + + async listPlayers(page = 1, pageSize = 20, parentId?: bigint) { + const where = { userType: 'PLAYER', ...(parentId ? { parentId } : {}) }; + const skip = (page - 1) * pageSize; + const [items, total] = await Promise.all([ + this.prisma.user.findMany({ + where, + include: { wallet: true }, + skip, + take: pageSize, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.user.count({ where }), + ]); + return { items, total, page, pageSize }; + } +} diff --git a/apps/api/src/wallet/wallet.module.ts b/apps/api/src/wallet/wallet.module.ts new file mode 100644 index 0000000..b813140 --- /dev/null +++ b/apps/api/src/wallet/wallet.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { WalletService } from './wallet.service'; + +@Module({ + providers: [WalletService], + exports: [WalletService], +}) +export class WalletModule {} diff --git a/apps/api/src/wallet/wallet.service.ts b/apps/api/src/wallet/wallet.service.ts new file mode 100644 index 0000000..262e60a --- /dev/null +++ b/apps/api/src/wallet/wallet.service.ts @@ -0,0 +1,222 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { Decimal } from '@prisma/client/runtime/library'; +import { generateTransactionId } from '../common/decorators'; + +@Injectable() +export class WalletService { + constructor(private prisma: PrismaService) {} + + async getWallet(userId: bigint) { + const wallet = await this.prisma.wallet.findUnique({ where: { userId } }); + if (!wallet) throw new BadRequestException('Wallet not found'); + return wallet; + } + + async createWallet(userId: bigint, currency = 'USD') { + return this.prisma.wallet.create({ + data: { userId, currency }, + }); + } + + private async lockWallet(tx: Parameters[0]>[0], userId: bigint) { + const wallets = await tx.$queryRaw>` + SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE + `; + if (!wallets.length) throw new BadRequestException('Wallet not found'); + return wallets[0]; + } + + async deposit( + userId: bigint, + amount: Decimal | number, + operatorId: bigint, + remark?: string, + referenceId?: string, + ) { + const amt = new Decimal(amount); + if (amt.lte(0)) throw new BadRequestException('Amount must be positive'); + + return this.prisma.$transaction(async (tx) => { + const w = await this.lockWallet(tx, userId); + const balanceBefore = new Decimal(w.available_balance); + const balanceAfter = balanceBefore.add(amt); + + await tx.wallet.update({ + where: { id: w.id }, + data: { + availableBalance: balanceAfter, + version: { increment: 1 }, + }, + }); + + await tx.walletTransaction.create({ + data: { + transactionId: generateTransactionId(), + userId, + walletId: w.id, + transactionType: 'MANUAL_DEPOSIT', + amount: amt, + balanceBefore, + balanceAfter, + frozenBefore: w.frozen_balance, + frozenAfter: w.frozen_balance, + referenceType: 'DEPOSIT', + referenceId, + operatorId, + remark, + }, + }); + + return { balanceAfter }; + }); + } + + async withdraw( + userId: bigint, + amount: Decimal | number, + operatorId: bigint, + remark?: string, + referenceId?: string, + ) { + const amt = new Decimal(amount); + if (amt.lte(0)) throw new BadRequestException('Amount must be positive'); + + return this.prisma.$transaction(async (tx) => { + const w = await this.lockWallet(tx, userId); + const balanceBefore = new Decimal(w.available_balance); + if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance'); + const balanceAfter = balanceBefore.sub(amt); + + await tx.wallet.update({ + where: { id: w.id }, + data: { + availableBalance: balanceAfter, + version: { increment: 1 }, + }, + }); + + await tx.walletTransaction.create({ + data: { + transactionId: generateTransactionId(), + userId, + walletId: w.id, + transactionType: 'MANUAL_WITHDRAW', + amount: amt.neg(), + balanceBefore, + balanceAfter, + frozenBefore: w.frozen_balance, + frozenAfter: w.frozen_balance, + referenceType: 'WITHDRAW', + referenceId, + operatorId, + remark, + }, + }); + + return { balanceAfter }; + }); + } + + async freezeForBet(userId: bigint, stake: Decimal | number, betId: string) { + const amt = new Decimal(stake); + + return this.prisma.$transaction(async (tx) => { + const w = await this.lockWallet(tx, userId); + const avail = new Decimal(w.available_balance); + if (avail.lt(amt)) throw new BadRequestException('Insufficient balance'); + + const balanceAfter = avail.sub(amt); + const frozenAfter = new Decimal(w.frozen_balance).add(amt); + + await tx.wallet.update({ + where: { id: w.id }, + data: { + availableBalance: balanceAfter, + frozenBalance: frozenAfter, + version: { increment: 1 }, + }, + }); + + await tx.walletTransaction.create({ + data: { + transactionId: generateTransactionId(), + userId, + walletId: w.id, + transactionType: 'BET_FREEZE', + amount: amt.neg(), + balanceBefore: avail, + balanceAfter, + frozenBefore: w.frozen_balance, + frozenAfter, + referenceType: 'BET', + referenceId: betId, + }, + }); + }); + } + + async settleBet( + userId: bigint, + stake: Decimal, + payout: Decimal, + betId: string, + result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE', + ) { + const txTypeMap: Record = { + WIN: 'BET_SETTLE_WIN', + LOSE: 'BET_SETTLE_LOSE', + PUSH: 'BET_SETTLE_PUSH', + VOID: 'BET_VOID_REFUND', + HALF_WIN: 'BET_SETTLE_WIN', + HALF_LOSE: 'BET_SETTLE_LOSE', + }; + + return this.prisma.$transaction(async (tx) => { + const w = await this.lockWallet(tx, userId); + const avail = new Decimal(w.available_balance); + const frozen = new Decimal(w.frozen_balance); + const frozenAfter = frozen.sub(stake); + const balanceAfter = avail.add(payout); + + await tx.wallet.update({ + where: { id: w.id }, + data: { + availableBalance: balanceAfter, + frozenBalance: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter, + version: { increment: 1 }, + }, + }); + + await tx.walletTransaction.create({ + data: { + transactionId: generateTransactionId(), + userId, + walletId: w.id, + transactionType: txTypeMap[result] || 'BET_SETTLE_WIN', + amount: payout, + balanceBefore: avail, + balanceAfter, + frozenBefore: frozen, + frozenAfter: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter, + referenceType: 'BET', + referenceId: betId, + }, + }); + }); + } + + async getTransactions(userId: bigint, page = 1, pageSize = 20) { + const skip = (page - 1) * pageSize; + const [items, total] = await Promise.all([ + this.prisma.walletTransaction.findMany({ + where: { userId }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }), + this.prisma.walletTransaction.count({ where: { userId } }), + ]); + return { items, total, page, pageSize }; + } +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..6efaa46 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*", "prisma/seed.ts"] +} diff --git a/apps/player/index.html b/apps/player/index.html new file mode 100644 index 0000000..1507e05 --- /dev/null +++ b/apps/player/index.html @@ -0,0 +1,12 @@ + + + + + + TheBet365 + + +
+ + + diff --git a/apps/player/package.json b/apps/player/package.json new file mode 100644 index 0000000..bd37dfc --- /dev/null +++ b/apps/player/package.json @@ -0,0 +1,25 @@ +{ + "name": "@thebet365/player", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 5173", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@thebet365/shared": "workspace:*", + "axios": "^1.7.9", + "pinia": "^2.3.1", + "vue": "^3.5.13", + "vue-i18n": "^11.1.1", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.0.11", + "vue-tsc": "^2.2.0" + } +} diff --git a/apps/player/src/App.vue b/apps/player/src/App.vue new file mode 100644 index 0000000..590d4ea --- /dev/null +++ b/apps/player/src/App.vue @@ -0,0 +1,6 @@ + + diff --git a/apps/player/src/api/index.ts b/apps/player/src/api/index.ts new file mode 100644 index 0000000..37ce008 --- /dev/null +++ b/apps/player/src/api/index.ts @@ -0,0 +1,22 @@ +import axios from 'axios'; + +const api = axios.create({ baseURL: '/api' }); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + +api.interceptors.response.use( + (res) => res, + (err) => { + if (err.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + } + return Promise.reject(err); + }, +); + +export default api; diff --git a/apps/player/src/components/BetSlipDrawer.vue b/apps/player/src/components/BetSlipDrawer.vue new file mode 100644 index 0000000..76cdd1b --- /dev/null +++ b/apps/player/src/components/BetSlipDrawer.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/apps/player/src/layouts/MainLayout.vue b/apps/player/src/layouts/MainLayout.vue new file mode 100644 index 0000000..2dbab66 --- /dev/null +++ b/apps/player/src/layouts/MainLayout.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/apps/player/src/main.ts b/apps/player/src/main.ts new file mode 100644 index 0000000..010d1a7 --- /dev/null +++ b/apps/player/src/main.ts @@ -0,0 +1,34 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import { createI18n } from 'vue-i18n'; +import App from './App.vue'; +import router from './router'; +import './styles.css'; + +const i18n = createI18n({ + legacy: false, + locale: localStorage.getItem('locale') || 'zh-CN', + fallbackLocale: 'en-US', + messages: { + 'zh-CN': { + nav: { home: '首页', football: '足球', my_bets: '我的投注', profile: '我的' }, + auth: { login: '登录', username: '账号', password: '密码' }, + wallet: { balance: '余额' }, + bet: { bet_slip: '投注单', stake: '投注金额', place_bet: '确认下注', parlay: '串关' }, + }, + 'en-US': { + nav: { home: 'Home', football: 'Football', my_bets: 'My Bets', profile: 'Profile' }, + auth: { login: 'Login', username: 'Username', password: 'Password' }, + wallet: { balance: 'Balance' }, + bet: { bet_slip: 'Bet Slip', stake: 'Stake', place_bet: 'Place Bet', parlay: 'Parlay' }, + }, + 'ms-MY': { + nav: { home: 'Laman Utama', football: 'Bola Sepak', my_bets: 'Pertaruhan Saya', profile: 'Profil' }, + auth: { login: 'Log Masuk', username: 'Nama Pengguna', password: 'Kata Laluan' }, + wallet: { balance: 'Baki' }, + bet: { bet_slip: 'Slip Pertaruhan', stake: 'Jumlah', place_bet: 'Letak Pertaruhan', parlay: 'Berganda' }, + }, + }, +}); + +createApp(App).use(createPinia()).use(router).use(i18n).mount('#app'); diff --git a/apps/player/src/router/index.ts b/apps/player/src/router/index.ts new file mode 100644 index 0000000..a1a8ebe --- /dev/null +++ b/apps/player/src/router/index.ts @@ -0,0 +1,29 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { useAuthStore } from './stores/auth'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: '/login', component: () => import('./views/LoginView.vue') }, + { + path: '/', + component: () => import('./layouts/MainLayout.vue'), + meta: { requiresAuth: true }, + children: [ + { path: '', component: () => import('./views/HomeView.vue') }, + { path: 'football', component: () => import('./views/FootballView.vue') }, + { path: 'match/:id', component: () => import('./views/MatchDetailView.vue') }, + { path: 'bets', component: () => import('./views/MyBetsView.vue') }, + { path: 'profile', component: () => import('./views/ProfileView.vue') }, + ], + }, + ], +}); + +router.beforeEach((to) => { + const auth = useAuthStore(); + if (to.meta.requiresAuth && !auth.token) return '/login'; + if (to.path === '/login' && auth.token) return '/'; +}); + +export default router; diff --git a/apps/player/src/stores/auth.ts b/apps/player/src/stores/auth.ts new file mode 100644 index 0000000..b94c721 --- /dev/null +++ b/apps/player/src/stores/auth.ts @@ -0,0 +1,25 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import api from '../api'; + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token') || ''); + const user = ref(JSON.parse(localStorage.getItem('user') || 'null')); + + async function login(username: string, password: string) { + const { data } = await api.post('/player/auth/login', { username, password }); + token.value = data.data.token; + user.value = data.data.user; + localStorage.setItem('token', token.value); + localStorage.setItem('user', JSON.stringify(user.value)); + } + + function logout() { + token.value = ''; + user.value = null; + localStorage.removeItem('token'); + localStorage.removeItem('user'); + } + + return { token, user, login, logout }; +}); diff --git a/apps/player/src/stores/betSlip.ts b/apps/player/src/stores/betSlip.ts new file mode 100644 index 0000000..2bdc078 --- /dev/null +++ b/apps/player/src/stores/betSlip.ts @@ -0,0 +1,74 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; + +export interface SlipItem { + selectionId: string; + oddsVersion: string; + matchId: string; + matchName: string; + selectionName: string; + odds: number; + marketType: string; +} + +export const useBetSlipStore = defineStore('betSlip', () => { + const items = ref([]); + const stake = ref(10); + const mode = ref<'single' | 'parlay'>('single'); + + const count = computed(() => items.value.length); + const isParlay = computed(() => items.value.length >= 2); + + function addItem(item: SlipItem) { + const existing = items.value.findIndex( + (i) => i.selectionId === item.selectionId, + ); + if (existing >= 0) { + items.value.splice(existing, 1); + return; + } + items.value.push(item); + if (items.value.length >= 2) mode.value = 'parlay'; + } + + function removeItem(selectionId: string) { + items.value = items.value.filter((i) => i.selectionId !== selectionId); + if (items.value.length < 2) mode.value = 'single'; + } + + function clear() { + items.value = []; + mode.value = 'single'; + } + + const totalOdds = computed(() => + items.value.reduce((acc, i) => acc * i.odds, 1), + ); + + const potentialReturn = computed(() => + mode.value === 'parlay' + ? stake.value * totalOdds.value + : items.value.length === 1 + ? stake.value * items.value[0].odds + : 0, + ); + + const hasSameMatch = computed(() => { + const matchIds = items.value.map((i) => i.matchId); + return new Set(matchIds).size !== matchIds.length; + }); + + return { + items, + stake, + mode, + count, + isParlay, + totalOdds, + potentialReturn, + hasSameMatch, + addItem, + removeItem, + clear, + }; +}); diff --git a/apps/player/src/styles.css b/apps/player/src/styles.css new file mode 100644 index 0000000..dc5a4e9 --- /dev/null +++ b/apps/player/src/styles.css @@ -0,0 +1,58 @@ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f1419; + color: #e8eaed; + min-height: 100vh; +} +:root { + --primary: #00a826; + --primary-dark: #008a1f; + --bg-card: #1a2332; + --bg-hover: #243044; + --text-muted: #8b95a5; + --border: #2d3a4d; + --danger: #ff4444; +} +a { color: inherit; text-decoration: none; } +button { + cursor: pointer; + border: none; + font-family: inherit; +} +input { + font-family: inherit; + background: var(--bg-card); + border: 1px solid var(--border); + color: #fff; + padding: 10px 12px; + border-radius: 6px; + width: 100%; +} +.btn-primary { + background: var(--primary); + color: #fff; + padding: 12px 24px; + border-radius: 6px; + font-weight: 600; + width: 100%; +} +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } +.odds-btn { + background: var(--bg-card); + border: 1px solid var(--border); + color: #fff; + padding: 8px 12px; + border-radius: 6px; + text-align: center; + min-width: 70px; +} +.odds-btn.selected { background: var(--primary); border-color: var(--primary); } +.odds-btn .label { font-size: 11px; color: var(--text-muted); } +.odds-btn .value { font-size: 15px; font-weight: 700; color: #ffd700; } +.card { + background: var(--bg-card); + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; +} diff --git a/apps/player/src/views/FootballView.vue b/apps/player/src/views/FootballView.vue new file mode 100644 index 0000000..9f694f7 --- /dev/null +++ b/apps/player/src/views/FootballView.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/apps/player/src/views/HomeView.vue b/apps/player/src/views/HomeView.vue new file mode 100644 index 0000000..7acfafc --- /dev/null +++ b/apps/player/src/views/HomeView.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/apps/player/src/views/LoginView.vue b/apps/player/src/views/LoginView.vue new file mode 100644 index 0000000..93dd1d6 --- /dev/null +++ b/apps/player/src/views/LoginView.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/apps/player/src/views/MatchDetailView.vue b/apps/player/src/views/MatchDetailView.vue new file mode 100644 index 0000000..3ee2dea --- /dev/null +++ b/apps/player/src/views/MatchDetailView.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/apps/player/src/views/MyBetsView.vue b/apps/player/src/views/MyBetsView.vue new file mode 100644 index 0000000..81caf90 --- /dev/null +++ b/apps/player/src/views/MyBetsView.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/apps/player/src/views/ProfileView.vue b/apps/player/src/views/ProfileView.vue new file mode 100644 index 0000000..98c5289 --- /dev/null +++ b/apps/player/src/views/ProfileView.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/apps/player/src/vite-env.d.ts b/apps/player/src/vite-env.d.ts new file mode 100644 index 0000000..aad3656 --- /dev/null +++ b/apps/player/src/vite-env.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent; + export default component; +} diff --git a/apps/player/tsconfig.json b/apps/player/tsconfig.json new file mode 100644 index 0000000..5b923ba --- /dev/null +++ b/apps/player/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/apps/player/vite.config.ts b/apps/player/vite.config.ts new file mode 100644 index 0000000..8577745 --- /dev/null +++ b/apps/player/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/api': { target: 'http://localhost:3000', changeOrigin: true }, + }, + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..aad37d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + postgres: + image: postgres:16-alpine + container_name: thebet365-postgres + environment: + POSTGRES_USER: thebet365 + POSTGRES_PASSWORD: thebet365 + POSTGRES_DB: thebet365 + ports: + - '5432:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U thebet365'] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: thebet365-redis + ports: + - '6379:6379' + volumes: + - redis_data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + redis_data: diff --git a/docs/UAT_CHECKLIST.md b/docs/UAT_CHECKLIST.md new file mode 100644 index 0000000..04611f9 --- /dev/null +++ b/docs/UAT_CHECKLIST.md @@ -0,0 +1,52 @@ +# UAT 测试与上线准备 + +## 测试账号 + +| 角色 | 用户名 | 密码 | 说明 | +|------|--------|------|------| +| 超级管理员 | admin | Admin@123 | 平台后台 | +| 一级代理 | agent1 | Agent@123 | 代理后台,授信 100000 | +| 二级代理 | agent2 | Agent@123 | 隶属 agent1 | +| 测试玩家 | player1 | Player@123 | 初始余额 1000 | + +## MVP 验收清单(18 项) + +- [ ] 玩家可登录、改密码、切换语言 +- [ ] 代理可创建直属玩家 +- [ ] 代理可给直属玩家上分/下分 +- [ ] 代理额度正确扣减和释放 +- [ ] 后台可创建比赛和盘口 +- [ ] 后台可批量录入波胆赔率 +- [ ] 前台可展示赛事和盘口 +- [ ] 玩家可单关下注 +- [ ] 玩家可 2-5 串 1 +- [ ] 同场串关被禁止 +- [ ] 四分之一盘口不能进入串关 +- [ ] 开赛自动封盘 +- [ ] 后台可录入比分并生成预览 +- [ ] 确认结算后钱包正确变化 +- [ ] 玩家可查看注单和账变 +- [ ] 后台可生成并发放返水 +- [ ] Banner/公告/走马灯可配置 +- [ ] 所有关键操作有日志 + +## 回归测试流程 + +1. 运行单元测试:`pnpm --filter @thebet365/api test` +2. 代理上分 → 玩家下注 → 封盘 → 录入比分 → 预览 → 确认结算 +3. 验证钱包余额与代理额度变化 +4. 测试串关限制(同场、四分之一球、冠军竞猜) +5. 测试返水批次生成与确认 + +## 备份与回滚 + +- PostgreSQL:每日 `pg_dump` 备份 +- 回滚:恢复备份 + 回退部署版本 +- 结算错误:使用重结算冲正流程,禁止直接改余额 + +## 后台操作培训要点 + +1. 赛事创建:联赛 → 球队 → 比赛 → 生成盘口模板 → 录入赔率 → 发布 +2. 结算:录入半场/全场比分 → 生成预览 → 核对派彩 → 确认 +3. 代理管理:授信额度 → 监控占用 → 负数时禁止继续放款 +4. 返水:选择周期 → 预览 → 财务确认 → 发放 diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..40937d8 Binary files /dev/null and b/logo.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..12ae68a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "thebet365", + "version": "1.0.0", + "private": true, + "description": "足球投注平台 MVP - Monorepo", + "scripts": { + "dev": "pnpm --parallel -r run dev", + "dev:api": "pnpm --filter @thebet365/api dev", + "dev:player": "pnpm --filter @thebet365/player dev", + "dev:admin": "pnpm --filter @thebet365/admin dev", + "dev:agent": "pnpm --filter @thebet365/agent dev", + "build": "pnpm -r run build", + "test": "pnpm -r run test", + "db:generate": "pnpm --filter @thebet365/api db:generate", + "db:migrate": "pnpm --filter @thebet365/api db:migrate", + "db:seed": "pnpm --filter @thebet365/api db:seed", + "db:studio": "pnpm --filter @thebet365/api db:studio" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..86b0301 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,14 @@ +{ + "name": "@thebet365/shared", + "version": "1.0.0", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..0bd883e --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,123 @@ +// User & Auth +export enum UserType { + PLAYER = 'PLAYER', + AGENT = 'AGENT', + ADMIN = 'ADMIN', +} + +export enum UserStatus { + ACTIVE = 'ACTIVE', + DISABLED = 'DISABLED', + LOCKED = 'LOCKED', +} + +export enum AgentLevel { + LEVEL_1 = 1, + LEVEL_2 = 2, +} + +// Match +export enum MatchStatus { + DRAFT = 'DRAFT', + PUBLISHED = 'PUBLISHED', + CLOSED = 'CLOSED', + PENDING_SETTLEMENT = 'PENDING_SETTLEMENT', + SETTLED = 'SETTLED', + CANCELLED = 'CANCELLED', + VOID = 'VOID', +} + +export enum MarketStatus { + OPEN = 'OPEN', + SUSPENDED = 'SUSPENDED', + CLOSED = 'CLOSED', +} + +export enum MarketType { + FT_CORRECT_SCORE = 'FT_CORRECT_SCORE', + HT_CORRECT_SCORE = 'HT_CORRECT_SCORE', + SH_CORRECT_SCORE = 'SH_CORRECT_SCORE', + FT_HANDICAP = 'FT_HANDICAP', + FT_OVER_UNDER = 'FT_OVER_UNDER', + FT_1X2 = 'FT_1X2', + FT_ODD_EVEN = 'FT_ODD_EVEN', + HT_HANDICAP = 'HT_HANDICAP', + HT_OVER_UNDER = 'HT_OVER_UNDER', + HT_1X2 = 'HT_1X2', + OUTRIGHT_WINNER = 'OUTRIGHT_WINNER', +} + +export enum Period { + FT = 'FT', + HT = 'HT', + SH = 'SH', + OUTRIGHT = 'OUTRIGHT', +} + +// Bet +export enum BetType { + SINGLE = 'SINGLE', + PARLAY = 'PARLAY', +} + +export enum BetStatus { + PENDING = 'PENDING', + WON = 'WON', + LOST = 'LOST', + PUSH = 'PUSH', + VOID = 'VOID', + CANCELLED = 'CANCELLED', + SETTLED = 'SETTLED', +} + +export enum SelectionResult { + WIN = 'WIN', + HALF_WIN = 'HALF_WIN', + PUSH = 'PUSH', + HALF_LOSE = 'HALF_LOSE', + LOSE = 'LOSE', + VOID = 'VOID', +} + +// Wallet +export enum WalletTransactionType { + MANUAL_DEPOSIT = 'MANUAL_DEPOSIT', + MANUAL_WITHDRAW = 'MANUAL_WITHDRAW', + BET_FREEZE = 'BET_FREEZE', + BET_SETTLE_WIN = 'BET_SETTLE_WIN', + BET_SETTLE_LOSE = 'BET_SETTLE_LOSE', + BET_SETTLE_PUSH = 'BET_SETTLE_PUSH', + BET_VOID_REFUND = 'BET_VOID_REFUND', + CASHBACK = 'CASHBACK', + RESETTLE_REVERSE = 'RESETTLE_REVERSE', + MANUAL_ADJUST = 'MANUAL_ADJUST', +} + +export enum WalletStatus { + ACTIVE = 'ACTIVE', + FROZEN = 'FROZEN', + DISABLED = 'DISABLED', +} + +// Locale +export const SUPPORTED_LOCALES = ['zh-CN', 'ms-MY', 'en-US'] as const; +export type Locale = (typeof SUPPORTED_LOCALES)[number]; +export const DEFAULT_LOCALE: Locale = 'en-US'; + +// Admin roles +export enum AdminRole { + SUPER_ADMIN = 'SUPER_ADMIN', + MATCH_ADMIN = 'MATCH_ADMIN', + FINANCE_ADMIN = 'FINANCE_ADMIN', + SUPPORT = 'SUPPORT', +} + +export const PARLAY_MIN_LEGS = 2; +export const PARLAY_MAX_LEGS = 5; + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..15999f9 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..aa843ba --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,7559 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/admin: + dependencies: + axios: + specifier: ^1.7.9 + version: 1.16.1 + element-plus: + specifier: ^2.9.3 + version: 2.14.1(vue@3.5.35(typescript@5.7.3)) + vue: + specifier: ^3.5.13 + version: 3.5.35(typescript@5.7.3) + vue-router: + specifier: ^4.5.0 + version: 4.6.4(vue@3.5.35(typescript@5.7.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0))(vue@3.5.35(typescript@5.7.3)) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + vite: + specifier: ^6.0.11 + version: 6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.7.3) + + apps/agent: + dependencies: + axios: + specifier: ^1.7.9 + version: 1.16.1 + element-plus: + specifier: ^2.9.3 + version: 2.14.1(vue@3.5.35(typescript@5.7.3)) + vue: + specifier: ^3.5.13 + version: 3.5.35(typescript@5.7.3) + vue-router: + specifier: ^4.5.0 + version: 4.6.4(vue@3.5.35(typescript@5.7.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0))(vue@3.5.35(typescript@5.7.3)) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + vite: + specifier: ^6.0.11 + version: 6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.7.3) + + apps/api: + dependencies: + '@nestjs/common': + specifier: ^11.0.6 + version: 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.0 + version: 4.0.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.6 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.0 + version: 11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/platform-express': + specifier: ^11.0.6 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + '@nestjs/schedule': + specifier: ^5.0.1 + version: 5.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + '@nestjs/swagger': + specifier: ^11.0.3 + version: 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + '@prisma/client': + specifier: ^6.3.1 + version: 6.19.3(prisma@6.19.3(typescript@5.7.3))(typescript@5.7.3) + '@thebet365/shared': + specifier: workspace:* + version: link:../../packages/shared + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.4 + decimal.js: + specifier: ^10.4.3 + version: 10.6.0 + ioredis: + specifier: ^5.4.2 + version: 5.11.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + uuid: + specifier: ^11.0.5 + version: 11.1.1 + devDependencies: + '@nestjs/cli': + specifier: ^11.0.2 + version: 11.0.21(@types/node@22.19.19) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.1.0(chokidar@4.0.3)(typescript@5.7.3) + '@nestjs/testing': + specifier: ^11.0.6 + version: 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/platform-express@11.1.24) + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.10.7 + version: 22.19.19 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + prisma: + specifier: ^6.3.1 + version: 6.19.3(typescript@5.7.3) + ts-jest: + specifier: ^29.2.5 + version: 29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)))(typescript@5.7.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.19.19)(typescript@5.7.3) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + + apps/player: + dependencies: + '@thebet365/shared': + specifier: workspace:* + version: link:../../packages/shared + axios: + specifier: ^1.7.9 + version: 1.16.1 + pinia: + specifier: ^2.3.1 + version: 2.3.1(typescript@5.7.3)(vue@3.5.35(typescript@5.7.3)) + vue: + specifier: ^3.5.13 + version: 3.5.35(typescript@5.7.3) + vue-i18n: + specifier: ^11.1.1 + version: 11.4.4(vue@3.5.35(typescript@5.7.3)) + vue-router: + specifier: ^4.5.0 + version: 4.6.4(vue@3.5.35(typescript@5.7.3)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.1(vite@6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0))(vue@3.5.35(typescript@5.7.3)) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + vite: + specifier: ^6.0.11 + version: 6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0) + vue-tsc: + specifier: ^2.2.0 + version: 2.2.0(typescript@5.7.3) + + packages/shared: + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.7.3 + +packages: + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.24': + resolution: {integrity: sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.0': + resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.0': + resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.0': + resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.0': + resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.0': + resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.0': + resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.0': + resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.0': + resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.0': + resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.0': + resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.0': + resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.0': + resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.0': + resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.0': + resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.0': + resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.0': + resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.0': + resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.0': + resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.0': + resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.0': + resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.0': + resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.0': + resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.0': + resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.0': + resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.0': + resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.1.2': + resolution: {integrity: sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.6': + resolution: {integrity: sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.7': + resolution: {integrity: sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.7': + resolution: {integrity: sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.9': + resolution: {integrity: sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.10': + resolution: {integrity: sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.1.6': + resolution: {integrity: sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.9': + resolution: {integrity: sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.9': + resolution: {integrity: sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.0.9': + resolution: {integrity: sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.0.9': + resolution: {integrity: sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.0.9': + resolution: {integrity: sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.4': + resolution: {integrity: sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@intlify/core-base@11.4.4': + resolution: {integrity: sha512-w/vItlylrAmhebkIbVl5YY8XMCtj8Mb2g70ttxktMYuf5AuRahgEHL2iLgLIsZBIbTSgs4hkUo7ucCL0uTJvOg==} + engines: {node: '>= 22'} + + '@intlify/devtools-types@11.4.4': + resolution: {integrity: sha512-PcBLmGmDQsTSVV911P8upzpcLJO1CNVYi/IH6bGnLR2nA+0L963+kXN1ZrisTEnbtw2ewN6HMMSldqzjronA0Q==} + engines: {node: '>= 22'} + + '@intlify/message-compiler@11.4.4': + resolution: {integrity: sha512-vn0OAV9pYkJlPPmgnsSm5eAG3mL0+9C/oaded2JY9jmxBbhmUXT3TcAUY8WRgLY9Hte7lkUJKpXrVlYjMXBD2w==} + engines: {node: '>= 22'} + + '@intlify/shared@11.4.4': + resolution: {integrity: sha512-QRUCHqda1U6aR14FR0vvXD4+4gj6+fm0AhAozvSuRCw0fCvrmCugWpgiR4xH2NI6s8am6N9p5OhirplsX8ZS3g==} + engines: {node: '>= 22'} + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.0.0': + resolution: {integrity: sha512-odQ+cjUpui6++a9Ua/oWn7CG0Af+EZe9weWZbfUQHTg7C3K9PCb0AnD4X7nyAe4WjfeZmVVyG5SJELMQaUbCtg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.0.0': + resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.0.0': + resolution: {integrity: sha512-ErShruvByUF7vphEtPugMAphCtDIDdfWh3DxpBLxPEtHhL/H5MaidHsOutnOUhKpPL7QA6/7GitjFgLOLeGa1A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.4.10': + resolution: {integrity: sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==} + + '@jridgewell/sourcemap-codec@1.4.14': + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@nestjs/cli@11.0.21': + resolution: {integrity: sha512-F8mV0Sj/zVEouzR3NxBuJy08YHTUOmC5Xdcx3qIIaJWzrm8Vw86CHkhkaPBJ5ewRMHPDCShPmhsfwhpCcjts3A==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.24': + resolution: {integrity: sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.1.24': + resolution: {integrity: sha512-K4bzT+lEdd0Hhcsw3jtk56QAW6s6skK3ViN7hIROSN0kUf4ROwWEAKopJID6yhPQxB45kDtP2wEcjzE8171J3g==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + + '@nestjs/platform-express@11.1.24': + resolution: {integrity: sha512-CeMKbRBm05aOBiWhIHWO2xDeHbxynBF9ySQv3gRjObz2N5+uJnYriAYkHvVqvC4JIydmMPmT5VdICFNlNz3qyA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schedule@5.0.1': + resolution: {integrity: sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + + '@nestjs/schematics@11.1.0': + resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + peerDependencies: + prettier: ^3.0.0 + typescript: '>=4.8.2' + peerDependenciesMeta: + prettier: + optional: true + + '@nestjs/swagger@11.4.4': + resolution: {integrity: sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/testing@11.1.24': + resolution: {integrity: sha512-+4M4UAnhtprBQN0J2uI6IP0wDqhy9aH8XCMu5SO8oCi0oB04YXA4a4PAEkxmsPn7gHW4dj1u4GFteNQOWgvTJw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} + hasBin: true + + '@prisma/client@6.19.3': + resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.3': + resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} + + '@prisma/debug@6.19.3': + resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': + resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} + + '@prisma/engines@6.19.3': + resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==} + + '@prisma/fetch-engine@6.19.3': + resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==} + + '@prisma/get-platform@6.19.3': + resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==} + + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sinclair/typebox@0.24.1': + resolution: {integrity: sha512-v59jW0KQIy4iMP0Pn7gcpQ48YDXa9vOopsRhaPWmsStTxaVNYIqFY3eK6y4PU+53hlQoaQ8pAxU4cPIaMFEJwg==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sinonjs/commons@2.0.0': + resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} + + '@sinonjs/fake-timers@10.0.2': + resolution: {integrity: sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@vitejs/plugin-vue@5.2.1': + resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} + + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} + + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} + + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.5.0': + resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} + + '@vue/devtools-api@6.6.3': + resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} + + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} + + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} + + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} + peerDependencies: + vue: 3.5.35 + + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + '@vueuse/core@14.3.0': + resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.3.0': + resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==} + + '@vueuse/shared@14.3.0': + resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==} + peerDependencies: + vue: ^3.5.0 + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.0: + resolution: {integrity: sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==} + engines: {node: '>= 6.0.0'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + alien-signals@0.4.9: + resolution: {integrity: sha512-piRGlMgQ65uRiY06mGU7I432AwPwAGf64TK1RXtM1Px4pPfLMTGI9TmsHTfioW1GukZRsNzkVQ/uHjhhd231Ow==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.2.1: + resolution: {integrity: sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==} + engines: {node: '>=8'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.0: + resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.2.0: + resolution: {integrity: sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cjs-module-lexer@1.0.0: + resolution: {integrity: sha512-bLSEjEwg4knnuXt7LIWegvgTOClk6ZonZY6g4CFGBly1EjRqVjTjI8Dwnb/dsu1PwJjYBKxnguE5bRTdk+bFOA==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.4: + resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.5.0: + resolution: {integrity: sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.0: + resolution: {integrity: sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.21.0: + resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + + element-plus@2.14.1: + resolution: {integrity: sha512-UFnm1+BckNi+azkKJ7L32q1uXs9ekr99Z9pWTQPeDR05jqEWUwQq51ro4kZMVrANbjknX3Z7ukCZwTi2T6Tr9A==} + peerDependencies: + vue: ^3.3.7 + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.0: + resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.0.0: + resolution: {integrity: sha512-OKAHGwaBqZ6I7bas0cnrrvomEL2d0yp2XXYQhhnVHfaqDaKStUBxjWtlGu/uI2tBqwb9sBMvaS41DSJFsRRJHQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-check@3.23.1: + resolution: {integrity: sha512-u/MudsoQEgBUZgR5N1v87vEgybeVYus9VnDVaIkxkkGP2jt54naghQ3PCQHJiogS8U/GavZCUPFfx3Xkp+NaHw==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.0.0: + resolution: {integrity: sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==} + engines: {node: '>=12'} + + fs-monkey@1.0.3: + resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-fresh@3.2.1: + resolution: {integrity: sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.11.0: + resolution: {integrity: sha512-EZBErytyVovD8f6pDfG3Kb37N6Y3lmDA9NNj+4+IP13CzzHGeX+OyeRM2Um13khRzoBSzzL+5lVnCX8V2RLeMg==} + engines: {node: '>=12.22.0'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.0.0: + resolution: {integrity: sha512-erkuYf1dQBHow3XJmS+bH6t9TZ0GwrSdQGauN8sTqyZlFByOjRadmHgTTcAHINeeSwxzGHN2ob3PXVvZphD7XQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.0.0: + resolution: {integrity: sha512-HtCxFHI8lQSbN1RppFjtl6DIrS+x4d3lOhpJljVxFEXob4lxlKon3FunW0XoGxNSvIoD00AfTFspnufpOqszrg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.0.0: + resolution: {integrity: sha512-4U0RdNV0TBTgVGzEchjryEpq4sqLO3gUQT7TEIbO5+q0K5MuiofOPcXk4GLpWviWByMRJjliQNMuzJ4YGT+oGQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.0.0: + resolution: {integrity: sha512-HMjW/pkFgi34LGKumjNDK03DYonV+nPMNUZ63rZX8PFdBkdIWUtOCEiaa7sAJkWrw5MyMVzSpa22NcOJjoQ3JQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.4.5: + resolution: {integrity: sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.13.1: + resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.0: + resolution: {integrity: sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + libphonenumber-js@1.13.4: + resolution: {integrity: sha512-/lhWr7vq8foWN9Apksnd9v8/cfwzW6g6qKOCo25XBGkNaVCHucXO57hLy4CWHGvytvLz6Nt3J5Gs8p3jlCGFXA==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.4.1: + resolution: {integrity: sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==} + engines: {node: '>= 4.0.0'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nypm@0.6.6: + resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.0: + resolution: {integrity: sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==} + engines: {node: '>=6'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@2.2.0: + resolution: {integrity: sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pinia@2.3.1: + resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@29.0.0: + resolution: {integrity: sha512-tMkFRn1vxRwZdiDETcveuNeonRKDg4doOvI+iyb1sOAtxYioGzRicqnsr+d3C/lLv9hBiM/2lDBi5ilR81h2bQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + prisma@6.19.3: + resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.0.0: + resolution: {integrity: sha512-rLSBxJjP+4DQOgcJAx6RZHT2he2pkhQdSnofG5VWyVl6GRq/K02ISOuOLcsMOrtKDIJb8JN2zm3FFzWNbezdPw==} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-is@18.0.0: + resolution: {integrity: sha512-yUcBYdBBbo3QiPsgYDcfQcIkGZHfxOaoE6HLSnr1sPzMhdyxusbfKOSUbSd/ocGi32dxcj366PsTj+5oggeKKw==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.1: + resolution: {integrity: sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==} + engines: {node: '>=10'} + + string-width@4.1.0: + resolution: {integrity: sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==} + engines: {node: '>=8'} + + string-width@4.2.0: + resolution: {integrity: sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==} + engines: {node: '>=8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.0: + resolution: {integrity: sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swagger-ui-dist@5.32.6: + resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-jest@29.4.11: + resolution: {integrity: sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.5.2: + resolution: {integrity: sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==} + engines: {node: '>=6'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.3: + resolution: {integrity: sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-i18n@11.4.4: + resolution: {integrity: sha512-gIbXVSFQV4jcSJxfwdZ5zSZmZ+12CnX0K3vBkRSd6Zn+HSzCp+QwUgPwpD/uN0oKNKI9RzlUXPKVedEuMgNG0A==} + engines: {node: '>= 22'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@2.2.0: + resolution: {integrity: sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack@5.106.0: + resolution: {integrity: sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.1: + resolution: {integrity: sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==} + engines: {node: '>= 8'} + hasBin: true + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + +snapshots: + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.24(@types/node@22.19.19)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@22.19.19) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@0.2.3': {} + + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@ctrl/tinycolor@4.2.0': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.35(typescript@5.7.3))': + dependencies: + vue: 3.5.35(typescript@5.7.3) + + '@esbuild/aix-ppc64@0.25.0': + optional: true + + '@esbuild/android-arm64@0.25.0': + optional: true + + '@esbuild/android-arm@0.25.0': + optional: true + + '@esbuild/android-x64@0.25.0': + optional: true + + '@esbuild/darwin-arm64@0.25.0': + optional: true + + '@esbuild/darwin-x64@0.25.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.0': + optional: true + + '@esbuild/freebsd-x64@0.25.0': + optional: true + + '@esbuild/linux-arm64@0.25.0': + optional: true + + '@esbuild/linux-arm@0.25.0': + optional: true + + '@esbuild/linux-ia32@0.25.0': + optional: true + + '@esbuild/linux-loong64@0.25.0': + optional: true + + '@esbuild/linux-mips64el@0.25.0': + optional: true + + '@esbuild/linux-ppc64@0.25.0': + optional: true + + '@esbuild/linux-riscv64@0.25.0': + optional: true + + '@esbuild/linux-s390x@0.25.0': + optional: true + + '@esbuild/linux-x64@0.25.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.0': + optional: true + + '@esbuild/netbsd-x64@0.25.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.0': + optional: true + + '@esbuild/openbsd-x64@0.25.0': + optional: true + + '@esbuild/sunos-x64@0.25.0': + optional: true + + '@esbuild/win32-arm64@0.25.0': + optional: true + + '@esbuild/win32-ia32@0.25.0': + optional: true + + '@esbuild/win32-x64@0.25.0': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.1.2(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.19.19) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/checkbox@4.3.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/confirm@5.1.21(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/confirm@5.1.6(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/core@10.1.7(@types/node@22.19.19)': + dependencies: + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.19.19) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/core@10.3.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/editor@4.2.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/editor@4.2.7(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + external-editor: 3.1.0 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/expand@4.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/expand@4.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.19)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/figures@1.0.10': {} + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.1.6(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/input@4.3.1(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/number@3.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/number@3.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/password@4.0.23(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/password@4.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/prompts@7.10.1(@types/node@22.19.19)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.19.19) + '@inquirer/confirm': 5.1.21(@types/node@22.19.19) + '@inquirer/editor': 4.2.23(@types/node@22.19.19) + '@inquirer/expand': 4.0.23(@types/node@22.19.19) + '@inquirer/input': 4.3.1(@types/node@22.19.19) + '@inquirer/number': 3.0.23(@types/node@22.19.19) + '@inquirer/password': 4.0.23(@types/node@22.19.19) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.19) + '@inquirer/search': 3.2.2(@types/node@22.19.19) + '@inquirer/select': 4.4.2(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/prompts@7.3.2(@types/node@22.19.19)': + dependencies: + '@inquirer/checkbox': 4.1.2(@types/node@22.19.19) + '@inquirer/confirm': 5.1.6(@types/node@22.19.19) + '@inquirer/editor': 4.2.7(@types/node@22.19.19) + '@inquirer/expand': 4.0.9(@types/node@22.19.19) + '@inquirer/input': 4.1.6(@types/node@22.19.19) + '@inquirer/number': 3.0.9(@types/node@22.19.19) + '@inquirer/password': 4.0.9(@types/node@22.19.19) + '@inquirer/rawlist': 4.0.9(@types/node@22.19.19) + '@inquirer/search': 3.0.9(@types/node@22.19.19) + '@inquirer/select': 4.0.9(@types/node@22.19.19) + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/rawlist@4.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/type': 3.0.4(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/rawlist@4.1.11(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/search@3.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/search@3.2.2(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/select@4.0.9(@types/node@22.19.19)': + dependencies: + '@inquirer/core': 10.1.7(@types/node@22.19.19) + '@inquirer/figures': 1.0.10 + '@inquirer/type': 3.0.4(@types/node@22.19.19) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/select@4.4.2(@types/node@22.19.19)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.19) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.19) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/type@3.0.10(@types/node@22.19.19)': + optionalDependencies: + '@types/node': 22.19.19 + + '@inquirer/type@3.0.4(@types/node@22.19.19)': + optionalDependencies: + '@types/node': 22.19.19 + + '@intlify/core-base@11.4.4': + dependencies: + '@intlify/devtools-types': 11.4.4 + '@intlify/message-compiler': 11.4.4 + '@intlify/shared': 11.4.4 + + '@intlify/devtools-types@11.4.4': + dependencies: + '@intlify/core-base': 11.4.4 + '@intlify/shared': 11.4.4 + + '@intlify/message-compiler@11.4.4': + dependencies: + '@intlify/shared': 11.4.4 + source-map-js: 1.2.1 + + '@intlify/shared@11.4.4': {} + + '@ioredis/commands@1.10.0': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.13.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + ansi-escapes: 4.2.1 + chalk: 4.1.2 + ci-info: 3.2.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.0.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.0.2 + '@types/node': 22.19.19 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.19.19 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.1 + strip-ansi: 6.0.0 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.0.0': + dependencies: + '@sinclair/typebox': 0.24.1 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.7 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.0.0': + dependencies: + '@jest/schemas': 29.0.0 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.19 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.19 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.10 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.4.10': {} + + '@jridgewell/sourcemap-codec@1.4.14': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.10 + + '@lukeed/csprng@1.1.0': {} + + '@microsoft/tsdoc@0.16.0': {} + + '@nestjs/cli@11.0.21(@types/node@22.19.19)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.24(@types/node@22.19.19)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@22.19.19) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.0 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/html' + - '@types/node' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - prettier + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + + '@nestjs/core@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + + '@nestjs/passport@11.0.5(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + + '@nestjs/platform-express@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schedule@5.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 3.5.0 + + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(typescript@5.7.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.7.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.6 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + + '@nestjs/testing@11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/platform-express@11.1.24)': + dependencies: + '@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24) + + '@nuxt/opencollective@0.4.1': + dependencies: + consola: 3.4.2 + + '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.7.3))(typescript@5.7.3)': + optionalDependencies: + prisma: 6.19.3(typescript@5.7.3) + typescript: 5.7.3 + + '@prisma/config@6.19.3': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.21.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.3': {} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} + + '@prisma/engines@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/fetch-engine': 6.19.3 + '@prisma/get-platform': 6.19.3 + + '@prisma/fetch-engine@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/get-platform': 6.19.3 + + '@prisma/get-platform@6.19.3': + dependencies: + '@prisma/debug': 6.19.3 + + '@rollup/rollup-android-arm-eabi@4.61.0': + optional: true + + '@rollup/rollup-android-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-x64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.0': + optional: true + + '@scarf/scarf@1.4.0': {} + + '@sinclair/typebox@0.24.1': {} + + '@sinclair/typebox@0.27.10': {} + + '@sinonjs/commons@2.0.0': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.0.2': + dependencies: + '@sinonjs/commons': 2.0.0 + + '@standard-schema/spec@1.1.0': {} + + '@sxzz/popperjs-es@2.11.8': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/bcryptjs@2.4.6': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.19 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.19 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 22.19.19 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.19.19 + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.0.0 + pretty-format: 29.0.0 + + '@types/json-schema@7.0.15': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.19.19 + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/luxon@3.4.2': {} + + '@types/ms@2.1.0': {} + + '@types/node@22.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.19 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.19 + + '@types/stack-utils@2.0.3': {} + + '@types/uuid@10.0.0': {} + + '@types/validator@13.15.10': {} + + '@types/web-bluetooth@0.0.21': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@vitejs/plugin-vue@5.2.1(vite@6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0))(vue@3.5.35(typescript@5.7.3))': + dependencies: + vite: 6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0) + vue: 3.5.35(typescript@5.7.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.35 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.35': + dependencies: + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-sfc@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.35': + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.5.0': {} + + '@vue/devtools-api@6.6.3': {} + + '@vue/devtools-api@6.6.4': {} + + '@vue/language-core@2.2.0(typescript@5.7.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.35 + alien-signals: 0.4.9 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.7.3 + + '@vue/reactivity@3.5.35': + dependencies: + '@vue/shared': 3.5.35 + + '@vue/runtime-core@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/runtime-dom@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@5.7.3))': + dependencies: + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@5.7.3) + + '@vue/shared@3.5.35': {} + + '@vueuse/core@14.3.0(vue@3.5.35(typescript@5.7.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.3.0 + '@vueuse/shared': 14.3.0(vue@3.5.35(typescript@5.7.3)) + vue: 3.5.35(typescript@5.7.3) + + '@vueuse/metadata@14.3.0': {} + + '@vueuse/shared@14.3.0(vue@3.5.35(typescript@5.7.3))': + dependencies: + vue: 3.5.35(typescript@5.7.3) + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@6.0.0: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.4.9: {} + + ansi-colors@4.1.3: {} + + ansi-escapes@4.2.1: + dependencies: + type-fest: 0.5.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.16.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + babel-jest@29.7.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.29.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@29.6.3(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + + balanced-match@1.0.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.33: {} + + bcryptjs@2.4.3: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.0 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.0 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.1 + rc9: 2.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001793: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@0.7.0: {} + + chardet@2.1.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@3.2.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + cjs-module-lexer@1.0.0: {} + + class-transformer@0.5.1: {} + + class-validator@0.14.4: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.13.4 + validator: 13.15.35 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.5.0: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.0 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + cluster-key-slot@1.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.0: {} + + commander@4.1.1: {} + + comment-json@5.0.0: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.2.0: + dependencies: + import-fresh: 3.2.1 + js-yaml: 4.2.0 + parse-json: 5.2.0 + path-type: 4.0.0 + + create-jest@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + cron@3.5.0: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.1 + + csstype@3.2.3: {} + + dayjs@1.11.21: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dedent@1.7.2: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defu@6.1.7: {} + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + diff@4.0.4: {} + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.4.5 + + dotenv@16.4.5: {} + + dotenv@16.6.1: {} + + dotenv@17.4.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + effect@3.21.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.1 + + electron-to-chromium@1.5.364: {} + + element-plus@2.14.1(vue@3.5.35(typescript@5.7.3)): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.35(typescript@5.7.3)) + '@floating-ui/dom': 1.7.6 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 14.3.0(vue@3.5.35(typescript@5.7.3)) + async-validator: 4.2.5 + dayjs: 1.11.21 + lodash: 4.18.1 + lodash-es: 4.18.1 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.35(typescript@5.7.3) + vue-component-type-helpers: 3.3.3 + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@7.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.25.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.0 + '@esbuild/android-arm': 0.25.0 + '@esbuild/android-arm64': 0.25.0 + '@esbuild/android-x64': 0.25.0 + '@esbuild/darwin-arm64': 0.25.0 + '@esbuild/darwin-x64': 0.25.0 + '@esbuild/freebsd-arm64': 0.25.0 + '@esbuild/freebsd-x64': 0.25.0 + '@esbuild/linux-arm': 0.25.0 + '@esbuild/linux-arm64': 0.25.0 + '@esbuild/linux-ia32': 0.25.0 + '@esbuild/linux-loong64': 0.25.0 + '@esbuild/linux-mips64el': 0.25.0 + '@esbuild/linux-ppc64': 0.25.0 + '@esbuild/linux-riscv64': 0.25.0 + '@esbuild/linux-s390x': 0.25.0 + '@esbuild/linux-x64': 0.25.0 + '@esbuild/netbsd-arm64': 0.25.0 + '@esbuild/netbsd-x64': 0.25.0 + '@esbuild/openbsd-arm64': 0.25.0 + '@esbuild/openbsd-x64': 0.25.0 + '@esbuild/sunos-x64': 0.25.0 + '@esbuild/win32-arm64': 0.25.0 + '@esbuild/win32-ia32': 0.25.0 + '@esbuild/win32-x64': 0.25.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + esprima@4.0.1: {} + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.0.0: + dependencies: + '@jest/expect-utils': 29.0.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.0.0 + jest-message-util: 29.0.0 + jest-util: 29.0.0 + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-check@3.23.1: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.2: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + follow-redirects@1.16.0: {} + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0): + dependencies: + '@babel/code-frame': 7.29.7 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.2.0 + deepmerge: 4.3.1 + fs-extra: 10.0.0 + memfs: 3.4.1 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.8.1 + tapable: 2.3.3 + typescript: 5.9.3 + webpack: 5.106.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.0.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.0.3: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.7 + node-fetch-native: 1.6.7 + nypm: 0.6.6 + pathe: 2.0.3 + + glob-to-regexp@0.4.1: {} + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + import-fresh@3.2.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ioredis@5.11.0: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)): + dependencies: + '@babel/core': 7.29.7 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 3.2.0 + deepmerge: 4.3.1 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.19 + ts-node: 10.9.2(@types/node@22.19.19)(typescript@5.7.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.0.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.0.0 + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.19.19 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.0.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.0.0 + jest-get-type: 29.6.3 + pretty-format: 29.0.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.0.0: + dependencies: + '@babel/code-frame': 7.29.7 + '@jest/types': 29.0.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.0.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.7 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.12 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + chalk: 4.1.2 + cjs-module-lexer: 1.0.0 + collect-v8-coverage: 1.0.3 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + jest-util@29.0.0: + dependencies: + '@jest/types': 29.0.0 + '@types/node': 22.19.19 + chalk: 4.1.2 + ci-info: 3.2.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + chalk: 4.1.2 + ci-info: 3.2.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.19 + ansi-escapes: 4.2.1 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.1 + + jest-worker@27.4.5: + dependencies: + '@types/node': 22.19.19 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.19.19 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.13.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + kleur@3.0.3: {} + + leven@3.1.0: {} + + libphonenumber-js@1.13.4: {} + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + loader-runner@4.3.2: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash-es@4.18.1: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.18.1 + lodash-es: 4.18.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + luxon@3.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.4.1: + dependencies: + fs-monkey: 1.0.3 + + memoize-one@6.0.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@2.0.0: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + node-abort-controller@3.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.18.1 + + node-fetch-native@1.6.7: {} + + node-int64@0.4.0: {} + + node-releases@2.0.46: {} + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nypm@0.6.6: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.2.4 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.0: + dependencies: + mimic-fn: 2.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.5.0 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.0 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-limit@2.2.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.2.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.0 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pause@0.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pinia@2.3.1(typescript@5.7.3)(vue@3.5.35(typescript@5.7.3)): + dependencies: + '@vue/devtools-api': 6.6.3 + vue: 3.5.35(typescript@5.7.3) + vue-demi: 0.14.10(vue@3.5.35(typescript@5.7.3)) + optionalDependencies: + typescript: 5.7.3 + transitivePeerDependencies: + - '@vue/composition-api' + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pluralize@8.0.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@29.0.0: + dependencies: + '@jest/schemas': 29.0.0 + ansi-styles: 5.2.0 + react-is: 18.0.0 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.0.0 + + prisma@6.19.3(typescript@5.7.3): + dependencies: + '@prisma/config': 6.19.3 + '@prisma/engines': 6.19.3 + optionalDependencies: + typescript: 5.7.3 + transitivePeerDependencies: + - magicast + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + pure-rand@6.0.0: {} + + pure-rand@6.1.0: {} + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc9@2.1.2: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-is@18.0.0: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.0 + signal-exit: 3.0.7 + + rollup@4.61.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.0 + '@rollup/rollup-android-arm64': 4.61.0 + '@rollup/rollup-darwin-arm64': 4.61.0 + '@rollup/rollup-darwin-x64': 4.61.0 + '@rollup/rollup-freebsd-arm64': 4.61.0 + '@rollup/rollup-freebsd-x64': 4.61.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 + '@rollup/rollup-linux-arm-musleabihf': 4.61.0 + '@rollup/rollup-linux-arm64-gnu': 4.61.0 + '@rollup/rollup-linux-arm64-musl': 4.61.0 + '@rollup/rollup-linux-loong64-gnu': 4.61.0 + '@rollup/rollup-linux-loong64-musl': 4.61.0 + '@rollup/rollup-linux-ppc64-gnu': 4.61.0 + '@rollup/rollup-linux-ppc64-musl': 4.61.0 + '@rollup/rollup-linux-riscv64-gnu': 4.61.0 + '@rollup/rollup-linux-riscv64-musl': 4.61.0 + '@rollup/rollup-linux-s390x-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-musl': 4.61.0 + '@rollup/rollup-openbsd-x64': 4.61.0 + '@rollup/rollup-openharmony-arm64': 4.61.0 + '@rollup/rollup-win32-arm64-msvc': 4.61.0 + '@rollup/rollup-win32-ia32-msvc': 4.61.0 + '@rollup/rollup-win32-x64-gnu': 4.61.0 + '@rollup/rollup-win32-x64-msvc': 4.61.0 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + semver@6.3.0: {} + + semver@6.3.1: {} + + semver@7.8.1: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + string-length@4.0.1: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.0 + + string-width@4.1.0: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 5.2.0 + + string-width@4.2.0: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.0: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swagger-ui-dist@5.32.6: + dependencies: + '@scarf/scarf': 1.4.0 + + symbol-observable@4.0.0: {} + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(webpack@5.106.0): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.4.5 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.106.0 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.0 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.0 + minimatch: 3.1.5 + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-jest@29.4.11(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)))(typescript@5.7.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 29.7.0(@types/node@22.19.19)(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.8.1 + type-fest: 4.41.0 + typescript: 5.7.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.7 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.7) + jest-util: 29.7.0 + + ts-node@10.9.2(@types/node@22.19.19)(typescript@5.7.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.19 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.7.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.22.1 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@0.5.2: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript@5.7.3: {} + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.1.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validator@13.15.35: {} + + vary@1.1.2: {} + + vite@6.4.2(@types/node@22.19.19)(jiti@2.7.0)(terser@5.48.0): + dependencies: + esbuild: 0.25.0 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.19.19 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.48.0 + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.3: {} + + vue-demi@0.14.10(vue@3.5.35(typescript@5.7.3)): + dependencies: + vue: 3.5.35(typescript@5.7.3) + + vue-i18n@11.4.4(vue@3.5.35(typescript@5.7.3)): + dependencies: + '@intlify/core-base': 11.4.4 + '@intlify/devtools-types': 11.4.4 + '@intlify/shared': 11.4.4 + '@vue/devtools-api': 6.5.0 + vue: 3.5.35(typescript@5.7.3) + + vue-router@4.6.4(vue@3.5.35(typescript@5.7.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.35(typescript@5.7.3) + + vue-tsc@2.2.0(typescript@5.7.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 2.2.0(typescript@5.7.3) + typescript: 5.7.3 + + vue@3.5.35(typescript@5.7.3): + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@5.7.3)) + '@vue/shared': 3.5.35 + optionalDependencies: + typescript: 5.7.3 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.5.0: {} + + webpack@5.106.0: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.22.1 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.2 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(webpack@5.106.0) + watchpack: 2.5.1 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + which@2.0.1: + dependencies: + isexe: 2.0.0 + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.1.0 + strip-ansi: 6.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4653ecc --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - 'apps/*' + - 'packages/*' +allowBuilds: + '@nestjs/core': set this to true or false + '@prisma/client': set this to true or false + '@prisma/engines': set this to true or false + '@scarf/scarf': set this to true or false + esbuild: set this to true or false + prisma: set this to true or false + vue-demi: set this to true or false diff --git a/足球投注平台产品需求与MVP实施计划_PRD_v1.2.md b/足球投注平台产品需求与MVP实施计划_PRD_v1.2.md new file mode 100644 index 0000000..897fddb --- /dev/null +++ b/足球投注平台产品需求与MVP实施计划_PRD_v1.2.md @@ -0,0 +1,1650 @@ +# 足球投注平台产品需求与 MVP 实施计划(PRD)v1.2 + +> 文档日期:2026-06-02 +> 文档状态:访谈结论定稿版 +> 适用对象:产品、研发、测试、UI/UX、运营、财务、代理运营 +> 业务前提:面向赌博合法化国家/地区的合法运营场景。本文不展开法律合规风险论证,只描述产品、业务、账务、权限、交互、技术实现和质量控制。 +> 第一版目标:**尽量简单、快速上线、优先保证下注、结算、账务和代理额度正确。** + +--- + +## 0. 本版结论摘要 + +本版根据访谈确认,采用以下默认方案作为第一版开发依据。 + +| 模块 | 第一版定稿方案 | +|---|---| +| 体育项目 | 只做足球,数据结构预留其他体育项目。 | +| 下注模式 | 只做赛前盘,不做滚球 / Live / In-Play。 | +| 赔率来源 | 后台手动创建赛事、盘口、赔率,不接自动赔率源。 | +| 开奖方式 | 后台手动录入半场比分、全场比分,系统生成结算预览,管理员确认后入账。 | +| 前台语言 | 简体中文 `zh-CN`、马来语 / Bahasa Melayu `ms-MY`、英文 `en-US`。 | +| UI 方向 | 参考成熟体育投注站的信息密度和 Bet Slip 交互:赛事列表、盘口分组、赔率按钮、投注单抽屉;不复制任何品牌素材。 | +| 支持玩法 | 波胆、上半场波胆、下半场波胆、全场让球、全场大/小、全场独赢盘、全场单/双、半场让球、半场大/小、半场独赢盘、冠军竞猜。 | +| 串关 | 支持基础 `2 串 1` 到 `5 串 1`;不做系统串关;不允许同场串关;冠军竞猜不允许串关;四分之一让球/大小球第一版不允许进入串关。 | +| 让球/大小球 | 单关支持整数、半球、四分之一球;串关只允许整数和半球盘口,降低结算复杂度。 | +| 代理层级 | 第一版最多 2 级代理:平台 → 一级代理 → 二级代理 → 玩家。 | +| 代理额度 | 采用代理信用额度池。玩家余额、玩家冻结投注金额、下级代理占用都会消耗代理额度;玩家输掉或下分释放额度。 | +| 玩家钱包 | 可用余额 + 冻结金额;下注冻结本金,结算后释放并派彩或扣除。 | +| 返水 | 第一版只做玩家返水;按有效投注额计算;后台生成报表,人工确认发放。 | +| 暂不做 | 滚球、系统串关、同场串关、自动数据源、自动赛果源、Cash Out、Edit Bet、Bet Builder、原生 App、无限代理层级、代理佣金自动结算。 | + +--- + +## 1. 项目目标与范围 + +### 1.1 产品目标 + +搭建一个足球投注平台,支持平台后台、代理后台、玩家前台三类核心端口。 + +核心业务流程: + +```mermaid +flowchart LR + A[平台创建代理/玩家] --> B[平台给代理授信额度] + B --> C[代理创建下级代理/玩家] + C --> D[代理给直属玩家上分] + D --> E[玩家前台登录] + E --> F[查看足球赛事与盘口] + F --> G[单关或串关下注] + G --> H[系统冻结玩家本金] + H --> I[后台录入半场/全场比分] + I --> J[系统生成结算预览] + J --> K[管理员确认结算] + K --> L[系统派彩/扣款/退水] + L --> M[玩家查看注单/账变/返水] +``` + +### 1.2 第一版产品原则 + +1. **先稳定后丰富**:第一版不追求 Bet365 的完整功能,只参考其成熟的信息架构与下注体验。 +2. **账务不可直接改余额**:所有余额变化必须来自账变流水,不允许直接编辑钱包余额。 +3. **赔率必须快照化**:玩家确认下注时锁定赔率、盘口、玩法、选项、赔率版本。 +4. **结算必须可回溯**:每张注单的结算依据、比分、操作人、时间、重结算记录都必须留存。 +5. **代理必须受额度控制**:代理不能无限给下级或玩家上分,额度可为负展示,但负数状态禁止继续放款。 +6. **多语言从底层设计**:界面文本、赛事名称、球队名称、Banner、公告、规则说明均支持多语言字段和 fallback。 +7. **第一版以移动端 H5 为主**:桌面端可用即可,移动端体验优先。 + +### 1.3 MVP 必须交付 + +| 模块 | 必须交付内容 | +|---|---| +| 玩家前台 | 登录、改密码、语言切换、赛事列表、赛事详情、投注单、单关下注、串关下注、我的投注、账变记录、返水记录、公告/Banner/走马灯。 | +| 代理后台 | 创建直属玩家、创建下级代理、给直属下级上分/下分、查看额度、查看下级玩家注单和报表。 | +| 平台后台 | 用户管理、代理管理、额度管理、账务流水、赛事管理、盘口赔率管理、结算预览、确认开奖、返水批次、报表、内容管理、多语言管理、操作日志。 | +| 注单引擎 | 支持指定 10 类足球玩法 + 冠军竞猜;支持单关和基础 2-5 串 1;赔率快照;余额冻结;幂等提交。 | +| 结算引擎 | 支持半场/全场比分结算、下半场净比分结算、亚洲让球、大小球、单双、波胆、串关结算、取消退款、重结算冲正。 | +| 账务系统 | 玩家钱包、代理额度池、账变流水、额度流水、返水入账、人工上分/下分。 | +| 系统安全 | 后台角色权限、关键操作二次确认、操作日志、软删除、登录失败锁定、接口幂等。 | + +### 1.4 第一版明确不做 + +| 暂不做功能 | 原因 | +|---|---| +| 滚球 / Live / In-Play | 涉及实时比分、暂停下注、赔率频繁变化、延迟确认,第一版风险高。 | +| 系统串关,如 3 串 4、4 串 11 | 结算复杂,测试量大。 | +| 同场串关 | 强相关投注,风控与结算争议较多。 | +| 四分之一球串关 | 会产生半赢/半输拆注,第一版先限制。 | +| 自动赔率源 / 自动赛果源 | 先验证业务闭环,避免接口成本。 | +| Cash Out / Edit Bet / Bet Builder | 复杂度高,作为二期体验增强。 | +| 无限代理层级 | 报表、额度和权限复杂度过高。 | +| 代理佣金自动结算 | 第一版先做玩家返水和代理报表。 | +| 原生 iOS/Android App | 第一版先做移动端 H5。 | + +--- + +## 2. 用户角色与权限 + +### 2.1 角色定义 + +| 角色 | 入口 | 说明 | +|---|---|---| +| 超级管理员 | 平台后台 | 拥有全部权限,管理系统配置、代理、玩家、账务、赛事、开奖、返水、报表。 | +| 赛事管理员 | 平台后台 | 创建赛事、联赛、球队、盘口、赔率、封盘、录入赛果、发起结算预览。 | +| 财务管理员 | 平台后台 | 玩家上分/下分、代理额度调整、账变查询、返水发放、财务报表。 | +| 客服/查询员 | 平台后台 | 查询玩家资料、投注历史、账变、重置密码,无账务执行权限。 | +| 一级代理 | 代理后台 | 创建二级代理和直属玩家,给直属下级分配额度或上分/下分,查看组织树报表。 | +| 二级代理 | 代理后台 | 创建直属玩家,给直属玩家上分/下分,查看直属玩家报表。 | +| 玩家 | 玩家前台 | 登录、查看赛事、下注、查看注单、查看账单、查看返水、修改密码。 | + +### 2.2 权限矩阵 + +| 功能 | 超级管理员 | 赛事管理员 | 财务管理员 | 客服 | 一级代理 | 二级代理 | 玩家 | +|---|---:|---:|---:|---:|---:|---:|---:| +| 创建后台账号 | 是 | 否 | 否 | 否 | 否 | 否 | 否 | +| 创建一级代理 | 是 | 否 | 否 | 否 | 否 | 否 | 否 | +| 创建二级代理 | 是 | 否 | 否 | 否 | 是 | 否 | 否 | +| 创建玩家 | 是 | 否 | 可选 | 可选 | 是 | 是 | 否 | +| 设置代理额度 | 是 | 否 | 是 | 否 | 仅下级 | 否 | 否 | +| 玩家上分/下分 | 是 | 否 | 是 | 否 | 直属下级 | 直属下级 | 否 | +| 赛事维护 | 是 | 是 | 否 | 查看 | 查看 | 查看 | 查看 | +| 赔率维护 | 是 | 是 | 否 | 查看 | 查看 | 查看 | 查看 | +| 赛事封盘 | 是 | 是 | 否 | 否 | 否 | 否 | 否 | +| 录入赛果 | 是 | 是 | 否 | 否 | 否 | 否 | 否 | +| 确认结算 | 是 | 是/可选 | 否 | 否 | 否 | 否 | 否 | +| 重结算 | 是 | 否 | 否 | 否 | 否 | 否 | 否 | +| 返水批次 | 是 | 否 | 是 | 查看 | 查看下级 | 查看下级 | 查看自己 | +| 报表 | 全量 | 赛事报表 | 财务报表 | 查询报表 | 组织树 | 直属玩家 | 自己 | +| Banner/公告/走马灯 | 是 | 是 | 否 | 否 | 否 | 否 | 查看 | + +### 2.3 代理权限边界 + +1. 代理只能创建和操作直属下级。 +2. 一级代理可以创建二级代理和直属玩家。 +3. 二级代理只能创建直属玩家。 +4. 一级代理不能直接给二级代理名下玩家上分,除非该玩家是一级代理直属玩家。 +5. 下级代理的额度、回水比例、单笔限额、日限额不能超过上级授权范围。 +6. 代理不能修改赛事、盘口、赔率、开奖结果。 +7. 代理不能撤销玩家已结算注单。 +8. 代理账号被禁用后,其下级玩家可以继续登录还是一起冻结,需要后台配置;MVP 默认只禁用该代理操作权限,不自动冻结玩家。 + +--- + +## 3. 多语言与本地化 + +### 3.1 支持语言 + +| 语言 | Locale | 前台显示 | 备注 | +|---|---|---|---| +| 简体中文 | `zh-CN` | 中文 | 默认候选语言。 | +| 马来语 / Bahasa Melayu | `ms-MY` | Bahasa Melayu | 用户所说“马来西亚语”按马来语处理。 | +| 英文 | `en-US` | English | 默认 fallback 语言。 | + +### 3.2 语言切换规则 + +1. 登录页、前台首页、玩家中心均提供语言切换。 +2. 未登录用户:语言选择保存到浏览器 localStorage / cookie。 +3. 已登录用户:语言选择保存到用户偏好 `user_preferences.locale`。 +4. 后台也支持同三种语言,但第一版后台可以优先中文 + 英文,马来语可后补。 +5. 内容缺失时 fallback 顺序:当前语言 → 英文 → 中文。 +6. 后台维护赛事、球队、联赛时,至少录入一个主语言名称,其他语言可为空。 + +### 3.3 多语言内容范围 + +| 内容 | 是否多语言 | 说明 | +|---|---:|---| +| 系统菜单、按钮、错误提示 | 是 | 使用语言包 key。 | +| 玩法名称、盘口名称 | 是 | 使用系统内置翻译。 | +| 联赛、球队、赛事标题 | 是 | 后台可录入多语言名称,缺失时 fallback。 | +| Banner 标题、图片、跳转 | 是 | 不同语言可使用不同图片。 | +| 公告/走马灯 | 是 | 后台按语言维护。 | +| 投注规则说明 | 是 | 必须三语,减少争议。 | +| 注单状态 | 是 | 使用统一枚举翻译。 | + +### 3.4 关键翻译 Key 示例 + +| Key | 中文 | Bahasa Melayu | English | +|---|---|---|---| +| nav.home | 首页 | Laman Utama | Home | +| nav.football | 足球 | Bola Sepak | Football | +| nav.my_bets | 我的投注 | Pertaruhan Saya | My Bets | +| auth.login | 登录 | Log Masuk | Login | +| auth.username | 账号 | Nama Pengguna | Username | +| wallet.balance | 余额 | Baki | Balance | +| bet.bet_slip | 投注单 | Slip Pertaruhan | Bet Slip | +| bet.stake | 投注金额 | Jumlah Pertaruhan | Stake | +| bet.place_bet | 确认下注 | Letak Pertaruhan | Place Bet | +| bet.parlay | 串关 | Pertaruhan Berganda | Accumulator | +| market.correct_score | 波胆 | Skor Tepat | Correct Score | +| market.ft_handicap | 全场让球 | Handicap Masa Penuh | Full Time Handicap | +| market.ft_over_under | 全场大/小 | Atas/Bawah Masa Penuh | Full Time Over/Under | +| market.ft_1x2 | 全场独赢盘 | 1X2 Masa Penuh | Full Time 1X2 | +| market.ft_odd_even | 全场单/双 | Ganjil/Genap Masa Penuh | Full Time Odd/Even | +| market.ht_handicap | 半场让球 | Handicap Separuh Masa | Half Time Handicap | +| market.ht_over_under | 半场大/小 | Atas/Bawah Separuh Masa | Half Time Over/Under | +| market.ht_1x2 | 半场独赢盘 | 1X2 Separuh Masa | Half Time 1X2 | +| error.odds_changed | 赔率已变化,请重新确认 | Odds telah berubah, sila sahkan semula | Odds changed, please confirm again | +| error.market_closed | 盘口已关闭 | Pasaran telah ditutup | Market closed | +| error.insufficient_balance | 余额不足 | Baki tidak mencukupi | Insufficient balance | + +--- + +## 4. 前台 UI / UX 设计 + +### 4.1 总体风格 + +界面可参考成熟体育投注平台的信息架构:深色或高对比背景、清晰赛事分组、赔率按钮突出、右侧或底部投注单、快速查看余额、热门赛事与公告入口。 + +需要注意:参考的是**交互模式和信息组织方式**,不是复制 Bet365 的商标、Logo、图标、图片、文案或视觉资产。 + +### 4.2 移动端优先布局 + +第一版建议优先实现移动端 H5。 + +```mermaid +flowchart TD + A[顶部栏: Logo/语言/余额] --> B[Banner] + B --> C[公告走马灯] + C --> D[体育类型: 足球] + D --> E[联赛筛选] + E --> F[赛事列表] + F --> G[点击赔率加入投注单] + G --> H[底部投注单浮层] + H --> I[确认下注] + I --> J[下注成功页] +``` + +底部导航建议: + +| Tab | 内容 | +|---|---| +| 首页 | Banner、公告、热门赛事、今日赛事。 | +| 足球 | 联赛筛选、赛事列表、赛事详情。 | +| 投注单 | 当前已选择投注项,支持单关/串关。 | +| 我的投注 | 未结算、已结算、已取消注单。 | +| 我的 | 余额、账单、返水、改密码、语言。 | + +### 4.3 桌面端布局 + +桌面端采用三栏结构: + +| 区域 | 内容 | +|---|---| +| 左侧 | 体育类型、联赛、日期筛选。 | +| 中间 | 赛事列表、盘口赔率、赛事详情。 | +| 右侧 | 投注单、余额、快捷投注金额、最近投注。 | + +### 4.4 首页模块 + +| 模块 | 第一版要求 | +|---|---| +| 顶部栏 | Logo、语言切换、登录/余额、我的投注入口。 | +| Banner | 后台配置图片、标题、跳转链接、排序、语言。 | +| 公告走马灯 | 后台配置,支持多语言,点击可查看详情。 | +| 热门赛事 | 后台可设为热门;按开赛时间排序。 | +| 今日赛事 | 默认展示当天可下注赛事。 | +| 联赛筛选 | 按联赛折叠/展开。 | +| 投注单入口 | 移动端固定底部浮层,显示已选数量。 | + +### 4.5 赛事列表页 + +列表页只展示核心盘口,避免页面过长。 + +| 列表页展示 | 说明 | +|---|---| +| 全场独赢盘 | 主胜 / 平 / 客胜。 | +| 全场让球 | 主队 / 客队,两项。 | +| 全场大/小 | 大 / 小,两项。 | +| 更多盘口入口 | 点击进入赛事详情页展示全部玩法。 | + +### 4.6 赛事详情页 + +赛事详情页按盘口分组展示: + +1. 全场独赢盘 +2. 全场让球 +3. 全场大/小 +4. 全场单/双 +5. 半场独赢盘 +6. 半场让球 +7. 半场大/小 +8. 波胆 +9. 上半场波胆 +10. 下半场波胆 + +每个盘口组支持折叠/展开。赔率按钮显示:选项名、盘口线、赔率。 + +### 4.7 投注单交互 + +| 场景 | 交互 | +|---|---| +| 用户点击赔率 | 加入投注单;同一场同一盘口重复点击则取消选择。 | +| 单个选择项 | 展示单关投注金额输入框。 | +| 多个选择项 | 展示每项单关输入 + 串关区域。MVP 可默认只打开串关输入。 | +| 同场多个选项 | 投注单提示“同一场比赛不能串关”,但可分别作为单关投注。 | +| 赔率变化 | 提交时校验赔率版本;变化则提示重新确认。 | +| 盘口封盘 | 提交时提示盘口已关闭并移除该选择项。 | +| 余额不足 | 禁止提交并提示余额不足。 | +| 下注成功 | 展示注单号、投注金额、赔率、预计返还、投注时间。 | + +--- + +## 5. 赛事、盘口与赔率管理 + +### 5.1 赛事结构 + +```mermaid +flowchart LR + A[体育类型: 足球] --> B[联赛] + B --> C[比赛 Match] + C --> D[盘口 Market] + D --> E[选项 Selection] + E --> F[赔率 Odds] +``` + +### 5.2 比赛字段 + +| 字段 | 说明 | +|---|---| +| match_id | 比赛 ID。 | +| sport_type | 第一版固定 `FOOTBALL`。 | +| league_id | 联赛 ID。 | +| home_team_id | 主队。 | +| away_team_id | 客队。 | +| start_time | 开赛时间。 | +| status | 比赛状态。 | +| is_hot | 是否热门。 | +| display_order | 排序。 | +| publish_time | 发布时间。 | +| close_time | 封盘时间。 | +| created_by / updated_by | 操作人。 | + +### 5.3 比赛状态流 + +| 状态 | 说明 | 可操作 | +|---|---|---| +| DRAFT 草稿 | 后台创建中,前台不可见。 | 编辑赛事、赔率。 | +| PUBLISHED 已发布 | 前台可见且可下注。 | 编辑赔率、手动封盘。 | +| CLOSED 已封盘 | 前台可见但不可下注。 | 录入赛果。 | +| PENDING_SETTLEMENT 待结算 | 已录入比分,等待结算确认。 | 查看结算预览、确认结算。 | +| SETTLED 已结算 | 已派彩/扣款。 | 超级管理员可发起重结算。 | +| CANCELLED 已取消 | 比赛取消,相关未结算注单退款。 | 查看退款流水。 | +| VOID 作废 | 异常赛事,不参与结算。 | 查看操作记录。 | + +### 5.4 自动封盘 + +1. 默认在 `start_time` 到达时自动封盘。 +2. 后台可提前手动封盘。 +3. 投注提交时必须二次校验比赛状态、盘口状态、选项状态、赔率版本。 +4. 后台开赛时间修改后,需要重新计算封盘任务。 + +### 5.5 赔率管理规则 + +1. 所有赔率使用十进制赔率 Decimal Odds,例如 `1.85`、`2.10`。 +2. 后台修改赔率时生成新的 `odds_version`。 +3. 已下注订单使用投注时快照,不受后续赔率修改影响。 +4. 赔率最低值、最高值后台配置,避免误填。 +5. 赔率必须大于 `1.00`。 +6. 已发布赛事修改赔率需二次确认。 +7. 支持批量编辑波胆赔率,类似表格/Excel 输入。 + +--- + +## 6. 第一版支持的下注类型 + +### 6.1 玩法总表 + +| 编码 | 中文名称 | 英文建议名 | 结算依据 | 是否支持单关 | 是否支持串关 | +|---|---|---|---|---:|---:| +| `FT_CORRECT_SCORE` | 波胆 | Full Time Correct Score | 全场比分 | 是 | 是 | +| `HT_CORRECT_SCORE` | 上半场波胆 | Half Time Correct Score | 半场比分 | 是 | 是 | +| `SH_CORRECT_SCORE` | 下半场波胆 | Second Half Correct Score | 下半场净比分 | 是 | 是 | +| `FT_HANDICAP` | 全场让球 | Full Time Asian Handicap | 全场比分 + 让球 | 是 | 条件支持 | +| `FT_OVER_UNDER` | 全场大/小 | Full Time Over/Under | 全场总进球 | 是 | 条件支持 | +| `FT_1X2` | 全场独赢盘 | Full Time 1X2 | 全场胜平负 | 是 | 是 | +| `FT_ODD_EVEN` | 全场单/双 | Full Time Odd/Even | 全场总进球单双 | 是 | 是 | +| `HT_HANDICAP` | 半场让球 | Half Time Asian Handicap | 半场比分 + 让球 | 是 | 条件支持 | +| `HT_OVER_UNDER` | 半场大/小 | Half Time Over/Under | 半场总进球 | 是 | 条件支持 | +| `HT_1X2` | 半场独赢盘 | Half Time 1X2 | 半场胜平负 | 是 | 是 | +| `OUTRIGHT_WINNER` | 冠军竞猜 | Outright Winner | 后台选择冠军 | 是 | 否 | + +串关条件支持说明: + +1. 只支持 `2 串 1` 到 `5 串 1`。 +2. 不允许同一场比赛的多个选项进入同一串关单。 +3. 四分之一让球 / 四分之一大小球不允许进入串关,例如 `-0.25`、`-0.75`、`2.25`、`2.75`。 +4. 冠军竞猜不允许进入串关。 +5. 已封盘、暂停、赔率变化未确认的选项不能提交。 + +### 6.2 波胆 + +波胆即预测准确比分。 + +| 类型 | 结算依据 | 示例 | +|---|---|---| +| 全场波胆 | 全场比分,90 分钟 + 全场伤停补时,不含加时和点球。 | 2-1、1-1、其他主胜。 | +| 上半场波胆 | 上半场比分,45 分钟 + 上半场伤停补时。 | 1-0、0-0、2-1。 | +| 下半场波胆 | 下半场净比分,即全场比分减去半场比分。 | 半场 1-1,全场 3-1,则下半场比分为 2-0。 | + +#### 6.2.1 波胆比分模板 + +为降低后台录入成本,第一版使用固定模板。 + +全场波胆建议模板: + +| 分类 | 选项 | +|---|---| +| 平局 | 0-0、1-1、2-2、3-3、4-4、其他平局 | +| 主胜 | 1-0、2-0、2-1、3-0、3-1、3-2、4-0、4-1、4-2、4-3、其他主胜 | +| 客胜 | 0-1、0-2、1-2、0-3、1-3、2-3、0-4、1-4、2-4、3-4、其他客胜 | + +半场 / 下半场波胆建议模板: + +| 分类 | 选项 | +|---|---| +| 平局 | 0-0、1-1、2-2、其他平局 | +| 主胜 | 1-0、2-0、2-1、3-0、其他主胜 | +| 客胜 | 0-1、0-2、1-2、0-3、其他客胜 | + +#### 6.2.2 “其他比分”结算规则 + +1. 如果最终比分命中模板中的具体比分,具体比分选项赢,其他比分选项输。 +2. 如果最终比分不在模板具体比分中,则按结果归类: + - 主队胜且不在具体比分中:其他主胜赢。 + - 客队胜且不在具体比分中:其他客胜赢。 + - 平局且不在具体比分中:其他平局赢。 + +### 6.3 全场让球 / 半场让球 + +让球采用亚洲让球两项盘。 + +| 项目 | 规则 | +|---|---| +| 全场让球 | 使用全场比分结算。 | +| 半场让球 | 使用半场比分结算。 | +| 盘口值 | 支持整数、半球、四分之一球。 | +| 单关 | 支持全赢、半赢、走水、半输、全输。 | +| 串关 | 第一版只允许整数和半球盘口进入串关;四分之一球不允许串关。 | + +盘口值示例:`0`、`-0.25`、`-0.5`、`-0.75`、`-1`、`+0.25`、`+0.5`、`+0.75`、`+1`。 + +让球结算公式: + +```text +adjusted_score = selected_team_goals + handicap - opponent_goals + +adjusted_score > 0 => 赢 +adjusted_score = 0 => 走水 +adjusted_score < 0 => 输 +``` + +四分之一球拆分规则: + +| 盘口 | 拆分为 | +|---|---| +| -0.25 | 0 和 -0.5 | +| +0.25 | 0 和 +0.5 | +| -0.75 | -0.5 和 -1 | +| +0.75 | +0.5 和 +1 | +| -1.25 | -1 和 -1.5 | +| +1.25 | +1 和 +1.5 | +| -1.75 | -1.5 和 -2 | +| +1.75 | +1.5 和 +2 | + +### 6.4 全场大/小 / 半场大/小 + +| 项目 | 规则 | +|---|---| +| 全场大/小 | 使用全场总进球数。 | +| 半场大/小 | 使用半场总进球数。 | +| 盘口值 | 支持整数、半球、四分之一球。 | +| 单关 | 支持全赢、半赢、走水、半输、全输。 | +| 串关 | 第一版只允许整数和半球盘口进入串关;四分之一球不允许串关。 | + +大小球结算示例: + +| 盘口 | 选择 | 实际总进球 | 结果 | +|---|---|---:|---| +| 2.5 | 大 | 3 | 赢 | +| 2.5 | 小 | 3 | 输 | +| 2 | 大 | 2 | 走水 | +| 2.25 | 大 | 2 | 半输 | +| 2.75 | 大 | 3 | 半赢 | + +四分之一大小球拆分规则: + +| 盘口 | 拆分为 | +|---|---| +| 2.25 | 2 和 2.5 | +| 2.75 | 2.5 和 3 | +| 3.25 | 3 和 3.5 | +| 3.75 | 3.5 和 4 | + +### 6.5 全场独赢盘 / 半场独赢盘 + +独赢盘采用足球三项盘: + +| 选项 | 说明 | +|---|---| +| 主胜 | 主队进球数大于客队。 | +| 平局 | 主队进球数等于客队。 | +| 客胜 | 主队进球数小于客队。 | + +全场独赢盘使用全场比分;半场独赢盘使用半场比分。 + +### 6.6 全场单/双 + +全场单/双按全场总进球数判断。 + +| 总进球 | 结果 | +|---:|---| +| 0 | 双 | +| 1 | 单 | +| 2 | 双 | +| 3 | 单 | +| 4 | 双 | + +### 6.7 冠军竞猜 + +冠军竞猜作为特殊长期盘实现。 + +| 项目 | 第一版规则 | +|---|---| +| 类型 | 特殊赛事 / 长期盘。 | +| 玩法 | 只支持选择冠军队伍。 | +| 赔率 | 后台手动维护。 | +| 封盘 | 后台手动封盘或到达配置时间自动封盘。 | +| 开奖 | 后台选择冠军队伍,生成结算预览,确认后派彩。 | +| 串关 | 第一版不允许串关。 | +| 取消 | 后台可整盘取消,未结算注单退还本金。 | + +--- + +## 7. 单关下注逻辑 + +### 7.1 单关注单流程 + +```mermaid +sequenceDiagram + participant U as 玩家 + participant FE as 前台 + participant API as 后端API + participant DB as 数据库 + + U->>FE: 选择赔率 + FE->>FE: 加入投注单 + U->>FE: 输入金额并确认下注 + FE->>API: 提交投注(selection_id, odds_version, stake, request_id) + API->>DB: 开启事务 + API->>DB: 校验用户状态/余额/盘口状态/赔率版本/限额 + API->>DB: 冻结投注本金 + API->>DB: 创建注单和账变流水 + API->>DB: 提交事务 + API-->>FE: 返回下注成功 + FE-->>U: 显示注单号和详情 +``` + +### 7.2 投注校验 + +提交下注时后端必须校验: + +1. 玩家账号状态为正常。 +2. 玩家所属代理链状态正常。 +3. 比赛状态为已发布且未封盘。 +4. 盘口状态为开放。 +5. 选项状态为开放。 +6. 前端提交的 `odds_version` 与数据库当前版本一致。 +7. 投注金额满足最小/最大单注限制。 +8. 玩家可用余额足够。 +9. 不超过玩家每日投注限额。 +10. 不超过盘口单项最大投注额。 +11. 不超过平台单注最高预计派彩。 +12. 请求幂等号 `request_id` 未被使用。 + +### 7.3 赔率变化处理 + +第一版采用严格校验方式: + +| 情况 | 处理 | +|---|---| +| 赔率未变化 | 正常下注。 | +| 赔率已变化 | 拒绝下注,返回最新赔率,提示玩家重新确认。 | +| 盘口已封盘 | 拒绝下注,从投注单移除。 | +| 选项已暂停 | 拒绝下注,提示该选项不可下注。 | + +### 7.4 投注金额与预计返还 + +十进制赔率下: + +```text +预计返还 = 投注金额 × 赔率 +预计盈利 = 投注金额 × (赔率 - 1) +``` + +示例:投注 100,赔率 1.85。 + +```text +预计返还 = 100 × 1.85 = 185 +预计盈利 = 100 × 0.85 = 85 +``` + +--- + +## 8. 串关玩法设计 + +### 8.1 第一版支持范围 + +| 项目 | 规则 | +|---|---| +| 串关类型 | 只支持 `2 串 1`、`3 串 1`、`4 串 1`、`5 串 1`。 | +| 系统串关 | 不支持。 | +| 同场串关 | 不支持。 | +| 最大串关场数 | 5 场。 | +| 最小串关场数 | 2 场。 | +| 冠军竞猜 | 不允许进入串关。 | +| 四分之一让球/大小球 | 不允许进入串关。 | +| 赔率 | 下单时锁定每个选项赔率。 | +| 结算 | 所有关联赛事结算后,系统自动结算串关。 | + +### 8.2 串关可选盘口 + +| 玩法 | 是否可串 | 备注 | +|---|---:|---| +| 全场独赢盘 | 是 | 主胜/平/客胜。 | +| 半场独赢盘 | 是 | 主胜/平/客胜。 | +| 全场单/双 | 是 | 单/双。 | +| 波胆 | 是 | 高赔率,需受最高派彩限制。 | +| 上半场波胆 | 是 | 高赔率,需受最高派彩限制。 | +| 下半场波胆 | 是 | 高赔率,需受最高派彩限制。 | +| 全场让球 | 条件支持 | 仅整数、半球盘口可串。 | +| 半场让球 | 条件支持 | 仅整数、半球盘口可串。 | +| 全场大/小 | 条件支持 | 仅整数、半球盘口可串。 | +| 半场大/小 | 条件支持 | 仅整数、半球盘口可串。 | +| 冠军竞猜 | 否 | 长期盘,不进入第一版串关。 | + +### 8.3 同场串关限制 + +同一张串关单中,不允许出现相同 `match_id` 的多个选择项。 + +示例:以下组合不允许作为串关: + +| 赛事 | 选择项 | +|---|---| +| 曼联 vs 切尔西 | 曼联胜 | +| 曼联 vs 切尔西 | 全场大 2.5 | + +原因:同一场比赛的不同盘口存在强相关性,第一版先禁止。 + +### 8.4 串关计算 + +如果所有选择项赢: + +```text +串关返还 = 投注金额 × 赔率1 × 赔率2 × ... × 赔率N +串关盈利 = 串关返还 - 投注金额 +``` + +如果任意一关输: + +```text +串关注单 = 输 +返还 = 0 +``` + +如果某一关走水 / 取消 / 作废: + +```text +该关赔率按 1.00 计算,其余关继续结算 +``` + +如果所有关都走水 / 取消 / 作废: + +```text +整张串关注单退还本金 +``` + +示例: + +| 关数 | 原赔率 | 结果 | 有效赔率 | +|---:|---:|---|---:| +| 1 | 1.80 | 赢 | 1.80 | +| 2 | 2.00 | 走水 | 1.00 | +| 3 | 1.90 | 赢 | 1.90 | + +投注 100: + +```text +返还 = 100 × 1.80 × 1.00 × 1.90 = 342 +``` + +### 8.5 串关限制配置 + +后台需支持以下配置: + +| 配置项 | 建议默认值 | +|---|---:| +| 最小串关数量 | 2 | +| 最大串关数量 | 5 | +| 单张串关最低投注额 | 1 | +| 单张串关最高投注额 | 视币种配置 | +| 单张串关最高派彩金额 | 视币种配置 | +| 玩家每日串关投注上限 | 视运营配置 | +| 玩家每日串关派彩上限 | 视运营配置 | +| 是否允许波胆串关 | 默认允许,但受派彩上限限制 | +| 是否允许同场串关 | 默认关闭 | +| 是否允许四分之一球串关 | 默认关闭 | + +--- + +## 9. 结算规则 + +### 9.1 比分口径 + +| 类型 | 口径 | +|---|---| +| 全场比分 | 90 分钟 + 全场伤停补时,不含加时赛和点球大战。 | +| 半场比分 | 上半场 45 分钟 + 上半场伤停补时。 | +| 下半场比分 | 全场比分 - 半场比分。 | +| 冠军竞猜 | 后台选择的最终冠军队伍。 | + +### 9.2 单关结算状态 + +| 状态 | 说明 | 返还公式 | +|---|---|---| +| WIN 全赢 | 下注选项完全赢 | `stake × odds` | +| HALF_WIN 半赢 | 四分之一球出现半赢 | `stake/2 × odds + stake/2` | +| PUSH 走水 | 盘口打平 | `stake` | +| HALF_LOSE 半输 | 四分之一球出现半输 | `stake/2` | +| LOSE 全输 | 下注选项输 | `0` | +| VOID 作废 | 比赛/盘口取消 | `stake` | + +### 9.3 结算流程 + +```mermaid +flowchart TD + A[管理员进入赛事结算页] --> B[录入半场比分] + B --> C[录入全场比分] + C --> D[点击生成结算预览] + D --> E[系统计算所有相关单关选择项] + E --> F[系统计算受影响串关] + F --> G[展示注单数量/应扣/应派/退款/异常] + G --> H{管理员确认?} + H -- 否 --> I[返回修改比分或取消] + H -- 是 --> J[系统事务入账] + J --> K[更新注单状态] + K --> L[生成账变流水] + L --> M[更新玩家钱包] + M --> N[更新代理额度占用] + N --> O[生成结算日志] +``` + +### 9.4 结算预览 + +确认入账前必须展示: + +| 指标 | 说明 | +|---|---| +| 比赛信息 | 联赛、主队、客队、开赛时间。 | +| 录入比分 | 半场比分、全场比分、自动计算下半场比分。 | +| 待结算单关注单数 | 本场相关单关数量。 | +| 待更新串关注单数 | 因本场结果影响的串关数量。 | +| 全赢金额 | 预计派彩金额。 | +| 输单金额 | 平台扣除本金金额。 | +| 走水/作废退款金额 | 需返还本金。 | +| 半赢/半输金额 | 四分之一盘口影响。 | +| 异常项 | 无赔率快照、注单状态异常、重复结算风险。 | +| 操作人 | 当前管理员。 | + +### 9.5 重结算与冲正 + +如果比分录错或结算错误,允许超级管理员重结算。 + +重结算规则: + +1. 不允许直接改余额。 +2. 必须生成反向冲正账变。 +3. 必须保留原结算记录和新结算记录。 +4. 必须记录操作原因。 +5. 重结算前生成影响预览:影响玩家数、注单数、需扣回金额、需补发金额。 +6. 如果玩家余额不足以扣回,应形成负余额或异常待处理;MVP 默认允许负余额展示并冻结提现/下分。 + +--- + +## 10. 钱包、账变与代理额度 + +### 10.1 玩家钱包 + +玩家钱包字段: + +| 字段 | 说明 | +|---|---| +| available_balance | 可用余额,可下注金额。 | +| frozen_balance | 冻结金额,未结算投注本金。 | +| total_balance | 总余额 = 可用余额 + 冻结金额。 | +| currency | 币种。 | +| status | 正常、冻结、禁用。 | + +下注时: + +```text +available_balance -= stake +frozen_balance += stake +``` + +结算赢: + +```text +frozen_balance -= stake +available_balance += payout +``` + +结算输: + +```text +frozen_balance -= stake +available_balance += 0 +``` + +走水/作废: + +```text +frozen_balance -= stake +available_balance += stake +``` + +### 10.2 账变流水类型 + +| 类型 | 说明 | 余额影响 | +|---|---|---| +| MANUAL_DEPOSIT | 后台/代理上分 | 增加可用余额。 | +| MANUAL_WITHDRAW | 后台/代理下分 | 减少可用余额。 | +| BET_FREEZE | 投注冻结 | 可用减少、冻结增加。 | +| BET_SETTLE_WIN | 赢单派彩 | 冻结减少、可用增加派彩金额。 | +| BET_SETTLE_LOSE | 输单扣除 | 冻结减少。 | +| BET_SETTLE_PUSH | 走水退回 | 冻结减少、可用增加本金。 | +| BET_VOID_REFUND | 作废退款 | 冻结减少、可用增加本金。 | +| CASHBACK | 玩家返水 | 增加可用余额。 | +| RESETTLE_REVERSE | 重结算冲正 | 按冲正方向变化。 | +| MANUAL_ADJUST | 人工调整 | 仅超级管理员,必须备注。 | + +账变流水必须包含: + +- `transaction_id` +- `user_id` +- `wallet_id` +- `transaction_type` +- `amount` +- `balance_before` +- `balance_after` +- `frozen_before` +- `frozen_after` +- `reference_type` +- `reference_id` +- `operator_id` +- `remark` +- `created_at` + +### 10.3 代理额度模型 + +第一版采用信用额度池,不把代理额度等同于真实资金账户。 + +核心字段: + +| 字段 | 说明 | +|---|---| +| credit_limit | 上级授予该代理的总额度。 | +| used_credit | 已占用额度。 | +| available_credit | 可用额度 = credit_limit - used_credit。 | +| direct_player_liability | 直属玩家总余额占用。 | +| child_agent_exposure | 下级代理占用。 | +| status | 正常、额度不足、冻结、禁用。 | + +推荐公式: + +```text +agent_used_credit = direct_player_total_balance + child_agent_exposure +agent_available_credit = agent_credit_limit - agent_used_credit + +child_agent_exposure = max(child_agent_credit_limit, child_agent_used_credit) +``` + +解释: + +1. 代理给玩家上分,玩家总余额增加,代理额度被占用。 +2. 玩家下注时,可用余额变成冻结金额,总余额不变,因此代理额度不变。 +3. 玩家输掉,玩家总余额减少,代理额度释放。 +4. 玩家赢了,玩家总余额增加,代理额度进一步占用。 +5. 代理给下级代理分配额度,会占用自己的额度。 +6. 如果下级代理因玩家赢钱导致实际占用超过其授信额度,上级看到的下级占用按更大的实际占用计算。 +7. 代理额度可以显示为负数,但负数时禁止继续上分或继续给下级分配额度。 + +### 10.4 代理额度示例 + +平台给一级代理 A 授信 10,000。 + +| 动作 | A 可用额度变化 | +|---|---:| +| 初始 | 10,000 | +| A 给玩家 P 上分 1,000 | 9,000 | +| A 给二级代理 B 分配额度 3,000 | 6,000 | +| P 下注 500 未结算 | 6,000,总余额不变 | +| P 输掉 500 | 6,500,玩家余额减少 | +| P 赢 2,000 | 4,500,玩家余额增加 | +| B 名下玩家赢钱导致 B 实际占用 4,000 | A 可用额度减少为 3,500,因为 B 实际占用超过授信 3,000 | + +### 10.5 上分/下分规则 + +| 场景 | 规则 | +|---|---| +| 平台给玩家上分 | 超级管理员/财务管理员可操作,直接入账并生成流水。 | +| 代理给直属玩家上分 | 受代理可用额度、单笔限额、日限额限制。 | +| 代理给玩家下分 | 只能操作直属玩家;玩家可用余额必须足够。 | +| 平台给代理授信 | 增加代理 `credit_limit`。 | +| 代理给下级代理分配额度 | 增加下级 `credit_limit`,占用本级额度。 | +| 回收下级代理额度 | 下级实际占用不能超过回收后的额度,否则禁止回收或提示会导致负额度。 | + +--- + +## 11. 返水机制 + +### 11.1 第一版返水策略 + +第一版只做玩家返水,不做自动代理佣金。 + +| 项目 | 规则 | +|---|---| +| 计算方式 | 按有效投注额 × 返水比例。 | +| 发放方式 | 后台生成返水报表,管理员确认后入账。 | +| 配置维度 | 可按玩家、代理线、玩法类型设置返水比例。MVP 可先按玩家或代理线配置。 | +| 发放周期 | 日结或周结,后台可选时间范围。 | +| 入账方式 | 生成 `CASHBACK` 账变流水,增加玩家可用余额。 | + +### 11.2 有效投注额口径 + +计入有效投注额: + +1. 已结算且非作废注单。 +2. 赢单、输单、半赢、半输均按原始投注金额计入。 +3. 串关注单按串关本金计入一次。 + +不计入有效投注额: + +1. 未结算注单。 +2. 已取消注单。 +3. 作废退款注单。 +4. 全走水注单。 +5. 重结算冲正流水本身。 + +### 11.3 返水批次流程 + +```mermaid +flowchart LR + A[选择周期] --> B[系统统计有效投注额] + B --> C[生成返水预览] + C --> D[财务确认] + D --> E[生成返水批次] + E --> F[逐玩家入账] + F --> G[生成账变流水] + G --> H[玩家前台查看返水记录] +``` + +--- + +## 12. 后台管理功能 + +### 12.1 平台后台模块 + +| 模块 | 功能 | +|---|---| +| 控制台 | 今日投注额、派彩额、平台盈亏、待结算赛事、异常注单、代理额度预警。 | +| 用户管理 | 玩家列表、新增玩家、禁用/启用、重置密码、查看钱包、查看注单、查看账变。 | +| 代理管理 | 新增代理、代理层级树、授信额度、回收额度、代理报表、禁用代理。 | +| 财务管理 | 上分、下分、账变流水、额度流水、人工调整、导出报表。 | +| 赛事管理 | 联赛、球队、比赛、开赛时间、热门标记、发布/封盘/取消。 | +| 盘口管理 | 为比赛启用玩法模板、录入盘口值、录入赔率、批量修改。 | +| 开奖结算 | 录入半场/全场比分、生成预览、确认结算、查看结算日志、重结算。 | +| 注单管理 | 单关注单、串关注单、按玩家/代理/赛事/状态筛选。 | +| 返水管理 | 返水比例配置、生成返水批次、确认发放、返水记录。 | +| 内容管理 | Banner、公告、走马灯、多语言内容、排序、上下架。 | +| 多语言管理 | 语言包 key、翻译缺失检查、导入导出。 | +| 系统设置 | 限额配置、赔率范围、串关规则、币种、小数位、时区。 | +| 操作日志 | 查询所有后台、代理后台关键操作记录。 | + +### 12.2 赛事创建流程 + +```mermaid +flowchart TD + A[创建联赛/选择联赛] --> B[创建比赛] + B --> C[选择主队/客队] + C --> D[设置开赛时间] + D --> E[选择玩法模板] + E --> F[系统生成盘口结构] + F --> G[批量录入赔率] + G --> H[保存草稿] + H --> I[发布比赛] +``` + +### 12.3 盘口模板 + +后台创建比赛时,建议提供一键生成模板: + +| 模板 | 自动生成内容 | +|---|---| +| 全场独赢盘 | 主胜、平局、客胜。 | +| 半场独赢盘 | 主胜、平局、客胜。 | +| 全场让球 | 主队、客队,需填写盘口值和赔率。 | +| 半场让球 | 主队、客队,需填写盘口值和赔率。 | +| 全场大/小 | 大、小,需填写盘口线和赔率。 | +| 半场大/小 | 大、小,需填写盘口线和赔率。 | +| 全场单/双 | 单、双。 | +| 波胆 | 按固定比分模板生成选项。 | +| 上半场波胆 | 按半场比分模板生成选项。 | +| 下半场波胆 | 按半场比分模板生成选项。 | +| 冠军竞猜 | 批量添加参赛队伍。 | + +### 12.4 高危操作二次确认 + +以下操作必须二次确认并写入操作日志: + +1. 大额上分/下分。 +2. 代理额度调整。 +3. 已发布赛事修改开赛时间。 +4. 已发布赛事修改赔率。 +5. 手动封盘。 +6. 取消赛事。 +7. 确认结算。 +8. 发起重结算。 +9. 发放返水批次。 +10. 禁用代理或玩家。 + +--- + +## 13. 数据库设计建议 + +### 13.1 核心表清单 + +| 表名 | 说明 | +|---|---| +| `users` | 用户基础表,包含玩家、代理、后台用户。 | +| `user_auth` | 登录账号、密码哈希、登录失败次数、最后登录。 | +| `roles` / `permissions` | 后台角色权限。 | +| `agent_profiles` | 代理资料、层级、上级、额度。 | +| `agent_closure` | 代理树闭包表,便于查询下级。 | +| `wallets` | 玩家钱包。 | +| `wallet_transactions` | 玩家账变流水。 | +| `agent_credit_transactions` | 代理额度流水。 | +| `leagues` | 联赛。 | +| `teams` | 球队。 | +| `matches` | 比赛。 | +| `match_scores` | 半场、全场、下半场比分。 | +| `market_types` | 玩法类型配置。 | +| `markets` | 比赛下的盘口。 | +| `market_selections` | 盘口选项。 | +| `odds_change_logs` | 赔率变更日志。 | +| `bets` | 注单主表,单关和串关。 | +| `bet_selections` | 注单明细,每个选择项一行。 | +| `settlement_batches` | 结算批次。 | +| `settlement_items` | 结算明细。 | +| `cashback_rules` | 返水规则。 | +| `cashback_batches` | 返水批次。 | +| `cashback_items` | 玩家返水明细。 | +| `contents` | Banner、公告、走马灯。 | +| `content_translations` | 内容多语言。 | +| `entity_translations` | 联赛、球队、赛事多语言名称。 | +| `i18n_messages` | 系统语言包。 | +| `audit_logs` | 操作日志。 | + +### 13.2 注单主表关键字段 + +```sql +CREATE TABLE bets ( + id BIGINT PRIMARY KEY, + bet_no VARCHAR(64) NOT NULL UNIQUE, + user_id BIGINT NOT NULL, + agent_id BIGINT, + bet_type VARCHAR(20) NOT NULL, -- SINGLE / PARLAY + stake DECIMAL(18, 4) NOT NULL, + total_odds DECIMAL(18, 6), + potential_return DECIMAL(18, 4), + actual_return DECIMAL(18, 4) DEFAULT 0, + status VARCHAR(32) NOT NULL, -- PENDING / WON / LOST / PUSH / VOID / CANCELLED / SETTLED + settlement_status VARCHAR(32), + currency VARCHAR(16) NOT NULL, + request_id VARCHAR(128) NOT NULL, + placed_at TIMESTAMP NOT NULL, + settled_at TIMESTAMP, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + UNIQUE(user_id, request_id) +); +``` + +### 13.3 注单明细表关键字段 + +```sql +CREATE TABLE bet_selections ( + id BIGINT PRIMARY KEY, + bet_id BIGINT NOT NULL, + match_id BIGINT, + market_id BIGINT NOT NULL, + selection_id BIGINT NOT NULL, + market_type VARCHAR(64) NOT NULL, + period VARCHAR(16), -- FT / HT / SH / OUTRIGHT + selection_name_snapshot VARCHAR(255) NOT NULL, + handicap_line DECIMAL(8, 2), + total_line DECIMAL(8, 2), + odds DECIMAL(18, 6) NOT NULL, + odds_version BIGINT NOT NULL, + result_status VARCHAR(32), -- WIN / HALF_WIN / PUSH / HALF_LOSE / LOSE / VOID + effective_odds DECIMAL(18, 6), + sort_order INT, + created_at TIMESTAMP NOT NULL +); +``` + +### 13.4 市场与选项字段 + +`markets`: + +| 字段 | 说明 | +|---|---| +| id | 盘口 ID。 | +| match_id | 所属比赛。 | +| market_type | 玩法类型。 | +| period | FT / HT / SH / OUTRIGHT。 | +| line_value | 让球线或大小球线,可为空。 | +| status | OPEN / SUSPENDED / CLOSED。 | +| allow_single | 是否允许单关。 | +| allow_parlay | 是否允许串关。 | +| sort_order | 排序。 | + +`market_selections`: + +| 字段 | 说明 | +|---|---| +| id | 选项 ID。 | +| market_id | 所属盘口。 | +| selection_code | HOME / DRAW / AWAY / OVER / UNDER / ODD / EVEN / SCORE_2_1 等。 | +| selection_name | 默认名称。 | +| odds | 当前赔率。 | +| odds_version | 当前赔率版本。 | +| status | OPEN / SUSPENDED / CLOSED。 | + +--- + +## 14. API 范围建议 + +### 14.1 玩家前台 API + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/player/auth/login` | 玩家登录。 | +| POST | `/api/player/auth/logout` | 退出登录。 | +| POST | `/api/player/auth/change-password` | 修改密码。 | +| GET | `/api/player/profile` | 玩家资料、余额、语言。 | +| POST | `/api/player/language` | 切换语言。 | +| GET | `/api/player/home` | 首页 Banner、公告、热门赛事。 | +| GET | `/api/player/matches` | 赛事列表。 | +| GET | `/api/player/matches/{id}` | 赛事详情和全部盘口。 | +| POST | `/api/player/bets/single` | 提交单关注单。 | +| POST | `/api/player/bets/parlay` | 提交串关注单。 | +| GET | `/api/player/bets` | 我的投注列表。 | +| GET | `/api/player/bets/{bet_no}` | 注单详情。 | +| GET | `/api/player/wallet/transactions` | 账变流水。 | +| GET | `/api/player/cashbacks` | 返水记录。 | + +### 14.2 平台后台 API + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/admin/auth/login` | 后台登录。 | +| GET | `/api/admin/dashboard` | 控制台指标。 | +| GET/POST | `/api/admin/users` | 玩家/用户管理。 | +| POST | `/api/admin/users/{id}/reset-password` | 重置密码。 | +| GET/POST | `/api/admin/agents` | 代理管理。 | +| POST | `/api/admin/agents/{id}/credit` | 调整代理额度。 | +| POST | `/api/admin/wallet/deposit` | 玩家上分。 | +| POST | `/api/admin/wallet/withdraw` | 玩家下分。 | +| GET | `/api/admin/wallet/transactions` | 账变查询。 | +| GET/POST | `/api/admin/leagues` | 联赛管理。 | +| GET/POST | `/api/admin/teams` | 球队管理。 | +| GET/POST | `/api/admin/matches` | 比赛管理。 | +| POST | `/api/admin/matches/{id}/publish` | 发布比赛。 | +| POST | `/api/admin/matches/{id}/close` | 封盘。 | +| POST | `/api/admin/matches/{id}/cancel` | 取消比赛。 | +| POST | `/api/admin/matches/{id}/markets/templates` | 生成盘口模板。 | +| PUT | `/api/admin/markets/{id}` | 更新盘口。 | +| PUT | `/api/admin/selections/{id}/odds` | 更新赔率。 | +| POST | `/api/admin/matches/{id}/settlement/preview` | 生成结算预览。 | +| POST | `/api/admin/matches/{id}/settlement/confirm` | 确认结算。 | +| POST | `/api/admin/matches/{id}/resettle/preview` | 重结算预览。 | +| POST | `/api/admin/matches/{id}/resettle/confirm` | 确认重结算。 | +| GET | `/api/admin/bets` | 注单查询。 | +| POST | `/api/admin/cashbacks/preview` | 返水预览。 | +| POST | `/api/admin/cashbacks/confirm` | 确认发放返水。 | +| GET/POST | `/api/admin/contents` | Banner、公告、走马灯。 | +| GET/POST | `/api/admin/i18n/messages` | 语言包管理。 | +| GET | `/api/admin/audit-logs` | 操作日志。 | + +### 14.3 代理后台 API + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/agent/auth/login` | 代理登录。 | +| GET | `/api/agent/profile` | 代理资料和额度。 | +| GET/POST | `/api/agent/players` | 直属玩家管理。 | +| GET/POST | `/api/agent/agents` | 下级代理管理,一级代理可用。 | +| POST | `/api/agent/players/{id}/deposit` | 给直属玩家上分。 | +| POST | `/api/agent/players/{id}/withdraw` | 给直属玩家下分。 | +| POST | `/api/agent/agents/{id}/credit` | 给直属下级代理分配额度。 | +| GET | `/api/agent/bets` | 下级注单查询。 | +| GET | `/api/agent/reports/summary` | 代理汇总报表。 | +| GET | `/api/agent/wallet-transactions` | 直属玩家账变查询。 | + +--- + +## 15. 幂等、事务与并发控制 + +### 15.1 必须幂等的操作 + +| 操作 | 幂等 Key | +|---|---| +| 单关下注 | `user_id + request_id` | +| 串关下注 | `user_id + request_id` | +| 玩家上分 | `operator_id + external_ref/request_id` | +| 玩家下分 | `operator_id + external_ref/request_id` | +| 代理额度调整 | `operator_id + request_id` | +| 确认结算 | `match_id + settlement_batch_id` | +| 重结算 | `match_id + resettle_batch_id` | +| 发放返水 | `cashback_batch_id` | + +### 15.2 下注事务要求 + +下注必须在单个数据库事务内完成: + +1. 锁定玩家钱包行。 +2. 校验余额。 +3. 校验比赛/盘口/选项状态。 +4. 校验赔率版本。 +5. 创建注单。 +6. 创建注单明细。 +7. 扣减可用余额、增加冻结金额。 +8. 创建账变流水。 +9. 提交事务。 + +### 15.3 结算事务要求 + +确认结算建议按批处理执行,但每批必须保证一致性: + +1. 锁定待结算注单。 +2. 防止重复结算。 +3. 计算返还金额。 +4. 更新注单状态。 +5. 更新钱包余额。 +6. 生成账变流水。 +7. 更新代理额度统计。 +8. 写入结算日志。 + +--- + +## 16. 报表需求 + +### 16.1 平台报表 + +| 报表 | 维度 | +|---|---| +| 平台投注汇总 | 日期、玩法、联赛、代理线、币种。 | +| 平台盈亏报表 | 投注额、派彩额、返水额、平台净盈亏。 | +| 待结算报表 | 比赛、开赛时间、待结算注单数、预估风险。 | +| 玩家报表 | 玩家余额、投注额、输赢、返水、上分/下分。 | +| 代理报表 | 代理额度、可用额度、下级余额、下级投注额、下级输赢。 | +| 盘口报表 | 每场比赛每个盘口投注分布。 | +| 串关报表 | 串关投注额、派彩额、最高风险。 | +| 账变报表 | 所有余额变动流水。 | + +### 16.2 代理报表 + +代理后台至少展示: + +1. 当前总额度。 +2. 已用额度。 +3. 可用额度。 +4. 直属玩家总余额。 +5. 下级代理额度占用。 +6. 今日投注额。 +7. 今日派彩额。 +8. 今日玩家输赢。 +9. 下级玩家注单列表。 +10. 上分/下分记录。 + +### 16.3 玩家报表 + +玩家前台展示: + +1. 当前余额。 +2. 未结算投注。 +3. 已结算投注。 +4. 赢/输/走水/作废状态。 +5. 账变流水。 +6. 返水记录。 + +--- + +## 17. 内容管理 + +### 17.1 Banner + +字段: + +| 字段 | 说明 | +|---|---| +| title | 标题,多语言。 | +| image_url | 图片,多语言可不同。 | +| link_type | 无跳转、赛事、公告、外链。 | +| link_target | 跳转目标。 | +| start_time / end_time | 展示时间。 | +| status | 草稿、启用、停用。 | +| sort_order | 排序。 | + +### 17.2 走马灯 / 公告 + +| 类型 | 说明 | +|---|---| +| 走马灯 | 首页顶部滚动短文案。 | +| 公告 | 可点击查看详情的完整内容。 | +| 系统通知 | 维护、封盘、活动等提示。 | + +公告必须支持三语内容。 + +--- + +## 18. 系统配置 + +### 18.1 投注限额配置 + +| 配置 | 维度 | +|---|---| +| 最小单注金额 | 全局 / 玩家等级 / 玩法。 | +| 最大单注金额 | 全局 / 玩家等级 / 玩法。 | +| 单张最高派彩 | 全局 / 玩法 / 串关。 | +| 玩家每日投注上限 | 玩家 / 代理线。 | +| 玩家每日派彩上限 | 玩家 / 代理线。 | +| 串关最小/最大场数 | 全局。 | +| 波胆最高派彩 | 玩法级。 | +| 赔率最小/最大值 | 全局。 | + +### 18.2 账号安全配置 + +| 配置 | 建议默认值 | +|---|---:| +| 玩家登录失败锁定次数 | 5 次。 | +| 玩家锁定时长 | 15 分钟。 | +| 后台登录失败锁定次数 | 5 次。 | +| 后台二次验证 | 建议开启 Google Authenticator 或 IP 白名单。 | +| 密码最小长度 | 8 位。 | +| 密码复杂度 | 至少字母 + 数字。 | +| 会话过期 | 玩家 24 小时,后台 2 小时。 | + +--- + +## 19. 测试用例重点 + +### 19.1 下注测试 + +| 编号 | 场景 | 预期 | +|---|---|---| +| B001 | 正常单关下注 | 创建注单、冻结本金、生成账变。 | +| B002 | 余额不足下注 | 拒绝下注,不生成注单。 | +| B003 | 赔率变化后提交 | 拒绝下注,提示重新确认。 | +| B004 | 封盘后提交 | 拒绝下注。 | +| B005 | 重复点击提交 | 只生成一张注单。 | +| B006 | 串关 2 串 1 正常提交 | 创建串关注单和多个明细。 | +| B007 | 串关包含同场多个选择项 | 拒绝提交。 | +| B008 | 串关包含四分之一球 | 拒绝提交。 | +| B009 | 串关超过 5 场 | 拒绝提交。 | +| B010 | 冠军竞猜加入串关 | 拒绝提交。 | + +### 19.2 结算测试 + +| 编号 | 场景 | 预期 | +|---|---|---| +| S001 | 全场独赢主胜 | 主胜注单赢,平/客输。 | +| S002 | 全场独赢平局 | 平局注单赢。 | +| S003 | 半场独赢 | 按半场比分结算。 | +| S004 | 全场单双 0-0 | 双赢。 | +| S005 | 全场波胆命中 2-1 | 2-1 赢,其他输。 | +| S006 | 全场波胆其他主胜 | 具体比分未列且主胜,其他主胜赢。 | +| S007 | 上半场波胆 | 按半场比分结算。 | +| S008 | 下半场波胆 | 按全场减半场后的净比分结算。 | +| S009 | 让球全赢 | 返还 stake × odds。 | +| S010 | 让球走水 | 退回本金。 | +| S011 | 让球半赢 | 返还 stake/2 × odds + stake/2。 | +| S012 | 让球半输 | 返还 stake/2。 | +| S013 | 大小球全赢 | 正确派彩。 | +| S014 | 大小球走水 | 退回本金。 | +| S015 | 大小球半赢/半输 | 正确拆分。 | +| S016 | 串关全中 | 按赔率连乘派彩。 | +| S017 | 串关一关输 | 整单输。 | +| S018 | 串关一关走水 | 该关赔率按 1.00。 | +| S019 | 串关全部走水 | 退回本金。 | +| S020 | 比赛取消 | 未结算注单退款。 | +| S021 | 重结算 | 生成冲正流水并更新新结果。 | + +### 19.3 代理额度测试 + +| 编号 | 场景 | 预期 | +|---|---|---| +| A001 | 代理给玩家上分 | 玩家余额增加,代理可用额度减少。 | +| A002 | 玩家下注未结算 | 玩家总余额不变,代理额度不变。 | +| A003 | 玩家输单 | 玩家总余额减少,代理额度释放。 | +| A004 | 玩家赢单 | 玩家总余额增加,代理额度占用增加。 | +| A005 | 代理额度为负 | 禁止继续上分和分配下级额度。 | +| A006 | 一级代理给二级代理分额 | 一级代理可用额度减少。 | +| A007 | 非直属玩家上分 | 拒绝操作。 | + +### 19.4 多语言测试 + +| 编号 | 场景 | 预期 | +|---|---|---| +| L001 | 切换中文 | 所有系统文案显示中文。 | +| L002 | 切换马来语 | 前台文案显示 Bahasa Melayu。 | +| L003 | 切换英文 | 前台文案显示英文。 | +| L004 | 球队名缺少马来语 | fallback 到英文或中文。 | +| L005 | Banner 不同语言图片 | 按当前语言展示正确图片。 | + +--- + +## 20. BUG 风险与控制方案 + +| 风险 | 可能后果 | 控制方案 | +|---|---|---| +| 赔率变化未校验 | 玩家按旧赔率下注,引发争议。 | 提交时校验 `odds_version`。 | +| 重复提交下注 | 重复扣款。 | `user_id + request_id` 幂等唯一约束。 | +| 结算重复执行 | 重复派彩。 | 结算批次唯一、注单状态锁、事务处理。 | +| 半赢/半输算错 | 钱包金额错误。 | 结算引擎单元测试覆盖四分之一盘口。 | +| 下半场波胆口径错 | 结算争议。 | 明确下半场净比分 = 全场 - 半场。 | +| 串关包含同场选项 | 强相关风险和规则争议。 | 后端强制校验 `match_id` 唯一。 | +| 玩家余额并发扣款 | 余额负数或超额下注。 | 钱包行级锁 / 乐观锁。 | +| 代理额度超用 | 代理无限放款。 | 每次上分、分额、派彩后重新计算额度。 | +| 人工改余额 | 账务不可追踪。 | 禁止直接改余额,只能账变。 | +| 比赛开始未封盘 | 开赛后仍可下注。 | 自动封盘任务 + 提交下注二次校验。 | +| 多语言缺失 | 页面显示混乱。 | fallback 机制 + 后台缺失翻译检查。 | +| 重结算扣回失败 | 玩家余额不足。 | 允许负余额并冻结下分,生成异常报表。 | + +--- + +## 21. 开发实施路径 + +### 21.1 推荐里程碑 + +#### 阶段 0:项目基础 + +交付内容: + +1. 前后台项目结构。 +2. 数据库初始化。 +3. 登录鉴权。 +4. RBAC 权限框架。 +5. 多语言框架。 +6. 操作日志基础能力。 +7. 钱包和账变基础表。 + +#### 阶段 1:账号、代理、钱包 + +交付内容: + +1. 平台后台创建玩家。 +2. 平台后台创建一级代理。 +3. 一级代理创建二级代理和玩家。 +4. 二级代理创建玩家。 +5. 代理额度池。 +6. 玩家上分/下分。 +7. 钱包流水。 +8. 代理额度流水。 + +验收重点:代理权限隔离、额度计算、账变一致性。 + +#### 阶段 2:赛事、盘口、赔率 + +交付内容: + +1. 联赛管理。 +2. 球队管理。 +3. 比赛管理。 +4. 盘口模板。 +5. 波胆批量赔率编辑。 +6. 发布/封盘/取消。 +7. 前台赛事列表。 +8. 前台赛事详情。 + +验收重点:赔率版本、封盘状态、多语言名称。 + +#### 阶段 3:下注引擎 + +交付内容: + +1. 投注单 UI。 +2. 单关下注。 +3. 串关下注。 +4. 投注限额。 +5. 赔率变化提示。 +6. 注单列表和详情。 +7. 钱包冻结。 + +验收重点:幂等、余额冻结、同场串关限制、四分之一球串关限制。 + +#### 阶段 4:结算引擎 + +交付内容: + +1. 录入半场/全场比分。 +2. 结算预览。 +3. 单关结算。 +4. 串关结算。 +5. 作废退款。 +6. 重结算冲正。 +7. 结算日志。 + +验收重点:所有玩法结算准确、重复结算防护、冲正账变。 + +#### 阶段 5:返水、报表、内容 + +交付内容: + +1. 返水规则。 +2. 返水预览。 +3. 返水确认发放。 +4. 平台报表。 +5. 代理报表。 +6. 玩家账单。 +7. Banner。 +8. 公告和走马灯。 +9. 多语言内容管理。 + +验收重点:返水有效投注额口径、报表与账变一致。 + +#### 阶段 6:UAT 与上线准备 + +交付内容: + +1. 测试账号。 +2. 测试赛事。 +3. 全玩法测试用例。 +4. 压测下注接口。 +5. 压测结算批次。 +6. 数据备份策略。 +7. 上线回滚方案。 +8. 后台操作培训文档。 + +### 21.2 MVP 上线验收清单 + +| 验收项 | 是否必须 | +|---|---:| +| 玩家可登录、改密码、切换语言 | 是 | +| 代理可创建直属玩家 | 是 | +| 代理可给直属玩家上分/下分 | 是 | +| 代理额度正确扣减和释放 | 是 | +| 后台可创建比赛和盘口 | 是 | +| 后台可批量录入波胆赔率 | 是 | +| 前台可展示赛事和盘口 | 是 | +| 玩家可单关下注 | 是 | +| 玩家可 2-5 串 1 | 是 | +| 同场串关被禁止 | 是 | +| 四分之一盘口不能进入串关 | 是 | +| 开赛自动封盘 | 是 | +| 后台可录入比分并生成预览 | 是 | +| 确认结算后钱包正确变化 | 是 | +| 玩家可查看注单和账变 | 是 | +| 后台可生成并发放返水 | 是 | +| Banner/公告/走马灯可配置 | 是 | +| 所有关键操作有日志 | 是 | + +--- + +## 22. 二期规划 + +第一版稳定后,再考虑以下能力: + +| 二期功能 | 说明 | +|---|---| +| 滚球 / Live | 实时盘口、赔率变化、下注延迟确认、危险进攻暂停。 | +| 系统串关 | 3 串 4、4 串 11、5 串 26 等组合。 | +| 同场组合投注 | 类似 Bet Builder 的同场多条件组合。 | +| Cash Out | 提前兑现。 | +| Edit Bet | 修改已下注串关。 | +| 自动赔率接口 | 接入第三方数据源。 | +| 自动赛果接口 | 自动获取比分和赛果。 | +| 多赔率格式 | Decimal、Malay、Hong Kong、Indonesian、American。 | +| 更多体育项目 | 篮球、网球、电竞等。 | +| 原生 App | iOS / Android。 | +| 代理佣金自动结算 | 按流水、净输赢、分层比例自动计算。 | +| 更复杂返水 | 按等级、玩法、时间段、活动自动返水。 | + +--- + +## 23. 研发交付建议 + +### 23.1 后端建议 + +1. 第一版采用单体后端,不建议微服务。 +2. 下注、结算、钱包、额度必须使用数据库事务。 +3. Redis 可用于会话、缓存、接口限流、封盘任务锁,但不能作为唯一账务来源。 +4. 账务金额使用 `DECIMAL(18,4)` 或更高精度,禁止使用浮点数。 +5. 赔率使用 `DECIMAL(18,6)`。 +6. 所有核心表使用软删除或状态字段,不物理删除。 +7. 所有账务接口必须写操作日志。 + +### 23.2 前端建议 + +1. 玩家前台优先移动端 H5。 +2. 投注单做成全局状态,切换页面不丢失。 +3. 投注提交后必须刷新余额和注单状态。 +4. 赔率变化时清晰提示,不自动替玩家接受新赔率。 +5. 多语言使用统一 i18n key,不把文案写死在组件里。 +6. 波胆区域需要折叠和分组,否则页面过长。 + +### 23.3 测试建议 + +1. 结算引擎必须做单元测试。 +2. 钱包和代理额度必须做集成测试。 +3. 串关必须做全流程测试。 +4. 上线前用固定比分回归全部玩法。 +5. 重结算必须单独测试冲正逻辑。 +6. 并发下注必须压测。 + +--- + +## 24. 最终 MVP 业务规则定稿 + +以下规则作为第一版不可变更的默认规则,除非重新评审: + +1. 足球普通赛事只按常规时间结算,即 90 分钟 + 伤停补时,不含加时和点球。 +2. 半场玩法只按上半场比分结算。 +3. 下半场波胆按下半场净比分结算。 +4. 全场 0-0 的单/双结果为双。 +5. 所有下注按下注时赔率快照结算。 +6. 赔率变化必须重新确认,不能默认接受。 +7. 开赛时间到达后自动封盘。 +8. 后台录入比分后必须先生成结算预览,再确认入账。 +9. 结算错误只能通过重结算和冲正账变修正,不能直接改余额。 +10. 串关只支持 2-5 串 1。 +11. 串关不允许同场。 +12. 串关不允许冠军竞猜。 +13. 串关不允许四分之一让球/大小球。 +14. 串关中走水或作废的关卡按赔率 1.00 处理。 +15. 玩家下注时冻结本金,结算后释放冻结并派彩/扣除/退款。 +16. 代理采用信用额度池。 +17. 玩家余额变化会影响所属代理额度占用。 +18. 代理额度可以为负展示,但负数时禁止继续上分和分配额度。 +19. 玩家返水按有效投注额计算,后台确认后发放。 +20. 第一版不做滚球、系统串关、自动赔率源、自动赛果源、Cash Out、Edit Bet、Bet Builder。