diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..03ce70b --- /dev/null +++ b/.clinerules @@ -0,0 +1,64 @@ +# OneERP Agent Rules Adapter + +This file mirrors the generic rules in `AGENTS.md` for tools that read repository rule files automatically. Do not put tool-specific policy here unless absolutely required. + +## Fixed Context + +Do not rediscover the whole repository for every task. Start from: + +- `AGENTS.md` +- `docs/README.md` +- `docs/architecture/ARCHITECTURE.md` +- `docs/architecture/DEVELOPMENT_WORKFLOW.md` +- `docs/architecture/STANDARDS.md` +- `docs/architecture/QUALITY_GATES.md` + +## Multi-Agent Task Policy + +Use one card per module boundary. Each card must declare: + +- goal +- scope +- forbidden files +- required reading +- acceptance command +- changed files +- residual risk + +## Conflict Policy + +Never assign two agents to edit these files at the same time: + +- `apps/api/prisma/schema.prisma` +- `apps/api/src/app.module.ts` +- `apps/web/src/lib/ui-schema.ts` +- `apps/api/src/core/**` +- `apps/web/src/components/core/**` +- `package-lock.json` + +## Branch Policy + +- Daily work: `agent//` from `develop` +- Integration: PR into `develop` +- Release: PR from `develop` into `main` +- Do not push directly to `main` + +## Validation Policy + +Before opening a PR, run the narrowest relevant test plus the full gate: + +```bash +npm run validate +``` + +When CI fails, classify the failure first: + +- dependency/install +- Prisma/migration +- TypeScript +- ESLint +- unit test +- build +- Docker/deploy + +Fix only the first real root cause. diff --git a/.dockerignore b/.dockerignore index 9ab8eae..e8e4c74 100644 --- a/.dockerignore +++ b/.dockerignore @@ -61,8 +61,10 @@ apps/desktop # Test files **/test **/__tests__ -**/*.spec.ts **/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx # Logs *.log diff --git a/.env.example b/.env.example index 4362bbb..ee4ce96 100644 --- a/.env.example +++ b/.env.example @@ -64,3 +64,24 @@ OPENAI_MODEL=gpt-4o-mini # 生产环境请根据实际部署地址配置,例如: # NEXT_PUBLIC_API_BASE_URL=https://api.example.com/api # NEXT_PUBLIC_API_BASE_URL=/api/proxy (搭配 Next.js rewrites 反向代理) + +# ============================================================ +# 生产环境专用变量 (仅 docker-compose.prod.yml 使用) +# ============================================================ +# 以下变量仅在生产部署时需要,在 .env 中按需配置 +# +# ---- Docker 镜像 ---- +# IMAGE_OWNER — 必填! GitHub 用户名或组织名,用于拉取 GHCR 镜像 +# IMAGE_TAG — 镜像标签,默认 latest;回滚时指定 commit SHA +# REGISTRY — 镜像仓库地址,默认 ghcr.io +# IMAGE_OWNER= +# IMAGE_TAG=latest +# REGISTRY=ghcr.io +# +# ---- CORS ---- +# CORS_ORIGINS — API 的 CORS 白名单 (逗号分隔),生产环境务必包含前端域名 +# CORS_ORIGINS=https://your-domain.com +# +# ---- Nginx (可选) ---- +# HTTP_PORT=80 +# HTTPS_PORT=443 \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ceab15e..e18ad19 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,9 +16,13 @@ ## 必读文档 +- `AGENTS.md` +- `docs/README.md` +- `docs/architecture/ARCHITECTURE.md` +- `docs/architecture/DEVELOPMENT_WORKFLOW.md` - `docs/architecture/STANDARDS.md` +- `docs/architecture/QUALITY_GATES.md` - `docs/plans/PROJECT_PLAN_AND_STATUS.md` -- `docs/plans/EXECUTION_PLAN.md` - `docs/plans/CORE_MODULES_DEV_PLAN.md` ## 后端开发规则 @@ -54,18 +58,19 @@ ## 提交前检查 -在仓库根目录执行: +在仓库根目录优先执行: ```bash -npm run lint -npm run test +npm run validate ``` -如果某个子应用未配置测试,至少确保可构建: +定位问题时可拆分执行: ```bash -cd apps/api && npm run build -cd ../web && npm run build +npm run typecheck +npm run lint +npm run test +npm run build ``` ## PR 要求 @@ -74,4 +79,4 @@ cd ../web && npm run build 2. 列出数据库变更和回滚方式(若有)。 3. 提供接口变更说明(若有)。 4. 前端改动附截图(若有)。 -5. 必须通过 CI。 \ No newline at end of file +5. 必须通过 CI。 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7823fc5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +## Summary / 摘要 + +### 中文 +- + +### English +- + +## Validation / 验证 + +### 中文 +- [ ] 已运行相关本地验证: +- [ ] 如未运行,请说明原因: + +### English +- [ ] Relevant local validation was run: +- [ ] If not run, explain why: + +## Review Notes / Review 备注 + +### 中文 +- 主要风险: +- 需要 reviewer 重点查看: + +### English +- Main risks: +- Areas needing reviewer attention: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e90561b..b7aa38a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,9 +82,8 @@ jobs: run: npm ci working-directory: apps/web - - name: Run Prisma migrations - run: npx prisma migrate deploy --schema=./prisma/schema.prisma - working-directory: apps/api + - name: Validate Prisma schema + run: npm --prefix apps/api run prisma:validate # Policy: ``npm run validate`` runs format -> lint -> typecheck -> test -> build. # The lint step must NOT use --fix in CI; developers use ``npm run lint:fix`` locally. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..b8fb65b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +name: CodeQL + +on: + pull_request: + branches: [main, develop] + push: + branches: [main, develop] + schedule: + - cron: "24 18 * * 0" + +jobs: + analyze: + name: Analyze JavaScript and TypeScript + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + build-mode: none + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9138113..7d9719d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,6 +15,11 @@ on: required: false default: "true" type: boolean + image_tag: + description: "镜像标签 (留空=latest, 填 commit SHA=回滚到指定版本)" + required: false + default: "" + type: string env: REGISTRY: ghcr.io @@ -23,6 +28,8 @@ env: jobs: # ---- Build & Push API Docker Image ---- build-api: + # 手动触发且未勾选 deploy_api 时跳过 + if: ${{ github.event_name != 'workflow_dispatch' || inputs.deploy_api }} runs-on: ubuntu-latest permissions: contents: read @@ -65,6 +72,8 @@ jobs: # ---- Build & Push Web Docker Image ---- build-web: + # 手动触发且未勾选 deploy_web 时跳过 + if: ${{ github.event_name != 'workflow_dispatch' || inputs.deploy_web }} runs-on: ubuntu-latest permissions: contents: read @@ -105,11 +114,12 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - # ---- Deploy to Server (placeholder — customize for your infra) ---- + # ---- Deploy to Production Server ---- deploy: needs: [build-api, build-web] + # 仅 main 分支 push 或手动触发时执行部署 + if: ${{ always() && (github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') && !failure() && !cancelled() && (needs.build-api.result == 'success' || needs.build-web.result == 'success') }} runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' environment: name: production @@ -126,36 +136,48 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # ---- Option A: Deploy via SSH (uncomment and configure) ---- - # - name: Deploy to server via SSH - # uses: appleboy/ssh-action@v1 - # with: - # host: ${{ secrets.DEPLOY_HOST }} - # username: ${{ secrets.DEPLOY_USER }} - # key: ${{ secrets.DEPLOY_SSH_KEY }} - # script: | - # cd /opt/oneerp - # docker compose pull - # docker compose up -d --remove-orphans - # docker image prune -f - - # ---- Option B: Deploy via Docker Compose on self-hosted runner ---- - # - name: Deploy with Docker Compose - # run: | - # docker compose -f docker-compose.prod.yml pull - # docker compose -f docker-compose.prod.yml up -d --remove-orphans - # docker image prune -f - - - name: Deployment placeholder + - name: Determine image tag + id: tag run: | - echo "::notice::Deploy step is configured as a placeholder." - echo "To enable automatic deployment, uncomment one of the options above." - echo "" - echo "Required secrets for SSH deployment:" - echo " - DEPLOY_HOST: Server IP or hostname" - echo " - DEPLOY_USER: SSH username" - echo " - DEPLOY_SSH_KEY: SSH private key" - echo "" - echo "Published images:" - echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api:latest" - echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:latest" + TAG="${{ inputs.image_tag }}" + if [ -z "$TAG" ]; then + TAG="latest" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "::notice::Deploying images with tag: $TAG" + + - name: Deploy to server via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd /opt/oneerp + echo "=== Pulling images (tag: ${{ steps.tag.outputs.tag }}) ===" + IMAGE_TAG=${{ steps.tag.outputs.tag }} docker compose -f docker-compose.prod.yml pull + echo "=== Starting services ===" + IMAGE_TAG=${{ steps.tag.outputs.tag }} docker compose -f docker-compose.prod.yml up -d --remove-orphans + echo "=== Waiting for health checks ===" + sleep 15 + IMAGE_TAG=${{ steps.tag.outputs.tag }} docker compose -f docker-compose.prod.yml ps + echo "=== Pruning old images ===" + docker image prune -f + + - name: Verify deployment + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd /opt/oneerp + echo "=== Checking container health ===" + docker compose -f docker-compose.prod.yml ps --format "table {{.Name}}\t{{.Status}}" + # 检查 API 是否可访问 + if curl -sf http://localhost:${API_PORT:-8000}/api/docs > /dev/null 2>&1; then + echo "✅ API health check passed" + else + echo "❌ API health check failed" + exit 1 + fi diff --git a/.gitignore b/.gitignore index abf28e6..b9b38c2 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,9 @@ __pycache__/ .git.backup-* + +# Playwright +test-results/ +playwright-report/ +blob-report/ +e2e-dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2f48d5b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,103 @@ +# OneERP Agent Rules + +This file is the cross-agent entry point for any AI coding agent. + +## Read First + +Before changing code, read these files in order: + +1. `README.md` +2. `docs/README.md` +3. `docs/architecture/ARCHITECTURE.md` +4. `docs/architecture/DEVELOPMENT_WORKFLOW.md` +5. `docs/architecture/STANDARDS.md` +6. `docs/architecture/QUALITY_GATES.md` +7. The module files and tests related to the task + +## Branch Rules + +- Do not use `main` for daily development. +- Use `develop` as the integration branch. +- Use one task branch per agent: `agent//`. +- Open feature PRs into `develop`; open release PRs from `develop` into `main`. + +Recommended scopes: + +- `agent/api/` +- `agent/web/` +- `agent/db/` +- `agent/test/` +- `agent/docs/` + +## Ownership Rules + +Only one agent may edit these high-conflict files at a time: + +- `apps/api/prisma/schema.prisma` +- `apps/api/src/app.module.ts` +- `apps/web/src/lib/ui-schema.ts` +- `apps/api/src/core/**` +- `apps/web/src/components/core/**` +- `package.json` +- `package-lock.json` +- subproject lockfiles + +## Engineering Rules + +- New database fields require Prisma schema, migration, DTO/service updates, and tests. +- Cross-module business effects must use events or workflow hooks. +- Business status transitions must go through the workflow layer. +- Inventory must be changed through movement/transaction records, not direct quantity edits. +- Finance must be changed through journal entries, not direct balance edits. +- API input must use DTO validation. +- Web UI should reuse metadata-driven engines and `apps/web/src/lib/api.ts`. + +## Validation + +Use the smallest relevant check while developing, then run the full gate before merge: + +```bash +npm run typecheck +npm run lint +npm run test +npm run build +npm run validate +``` + +If `npm run validate` fails, fix the first real failure before making more changes. + +## Review Output + +When an AI agent writes a code review, PR summary, or handoff review, it must include both Chinese and English sections. Keep the content equivalent, concise, and actionable. + +Required format: + +```md +## Review / 代码审查 + +### 中文 +- 问题: +- 风险: +- 建议: +- 验证: + +### English +- Findings: +- Risks: +- Suggestions: +- Verification: +``` + +If there are no findings, state that clearly in both languages and still mention remaining test gaps or residual risk. + +## Task Card Template + +```md +Goal: +Scope: +Forbidden files: +Read first: +Acceptance commands: +Related docs: +Related files: +``` diff --git a/COMMIT_MSG.txt b/COMMIT_MSG.txt new file mode 100644 index 0000000..e86a8d1 --- /dev/null +++ b/COMMIT_MSG.txt @@ -0,0 +1 @@ +fix(api): fix prettier linting issues in metadata service diff --git a/README.md b/README.md index e9a090d..c80854b 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,11 @@ flowchart LR - AI Command Bar:自然语言触发业务动作(支持 dry-run 草稿确认) - Chat2Dash:自然语言转图表洞察 - Chat2SQL:自然语言转查询语句(只读场景) -- OCR Draft(规划中):附件识别并生成草稿单据 +- Document Draft:附件文件名解析生成发票草稿(LLM 降级为规则兜底) ## Roadmap -- [ ] P0: 税务引擎(税码、税率、含税/未税) +- [x] P0: 税务引擎(税码、税率、含税/未税)—— `TaxCode` 模型 + `finance.service.resolveTaxCode()` 已落地 - [ ] P0: 采购全链路(询价、采购单、收货、应付) - [ ] P1: 库存单据头(Stock Picking/Wave) - [ ] P1: 多币种与汇率重估 @@ -146,19 +146,24 @@ flowchart LR 欢迎贡献代码、文档和测试: -1. Fork 本仓库 -2. 新建分支(`agent/backend/*`、`agent/frontend/*`、`agent/db/*`) -3. 提交遵循 Conventional Commits -4. 提交 PR 并通过 CI +1. 从 `develop` 新建任务分支(例如 `agent/api/purchase-order`、`agent/web/finance-page`、`agent/db/rbac-models`) +2. 单个 PR 只处理一个业务边界,避免多个 agent 同时修改 `schema.prisma`、`app.module.ts`、`ui-schema.ts`、`core/**` 或 lockfile +3. 提交前在根目录运行 `npm run validate` +4. 提交遵循 Conventional Commits +5. 功能 PR 合入 `develop`,稳定后由 `develop` 发版 PR 合入 `main` 详细规范见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 ## 文档入口 +- [docs/architecture/ARCHITECTURE.md](./docs/architecture/ARCHITECTURE.md) +- [docs/README.md](./docs/README.md) +- [AGENTS.md](./AGENTS.md) - [docs/architecture/STANDARDS.md](./docs/architecture/STANDARDS.md) +- [docs/architecture/DEVELOPMENT_WORKFLOW.md](./docs/architecture/DEVELOPMENT_WORKFLOW.md) +- [docs/architecture/QUALITY_GATES.md](./docs/architecture/QUALITY_GATES.md) +- [docs/runbooks/README.md](./docs/runbooks/README.md) - [docs/plans/PROJECT_PLAN_AND_STATUS.md](./docs/plans/PROJECT_PLAN_AND_STATUS.md) -- [docs/plans/PROJECT_PLAN.md](./docs/plans/PROJECT_PLAN.md) -- [docs/plans/EXECUTION_PLAN.md](./docs/plans/EXECUTION_PLAN.md) - [docs/plans/CORE_MODULES_DEV_PLAN.md](./docs/plans/CORE_MODULES_DEV_PLAN.md) - [docs/AI_INSTRUCTIONS.md](./docs/AI_INSTRUCTIONS.md) diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs index f11ecfe..faf1e87 100644 --- a/apps/api/eslint.config.mjs +++ b/apps/api/eslint.config.mjs @@ -32,4 +32,14 @@ export default tseslint.config( "prettier/prettier": ["error", { endOfLine: "auto" }], }, }, + { + files: ['**/*.spec.ts', 'test/**/*.ts'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + }, + }, ); diff --git a/apps/api/package-lock.json b/apps/api/package-lock.json index 5674926..2fb9130 100644 --- a/apps/api/package-lock.json +++ b/apps/api/package-lock.json @@ -71,16 +71,16 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz", + "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -99,9 +99,9 @@ } }, "node_modules/@angular-devkit/core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -133,13 +133,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", - "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz", + "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.24", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -152,14 +152,14 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", - "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "version": "19.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.24.tgz", + "integrity": "sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", "@inquirer/prompts": "7.3.2", "ansi-colors": "4.1.3", "symbol-observable": "4.0.0", @@ -1824,9 +1824,9 @@ } }, "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -2125,15 +2125,15 @@ } }, "node_modules/@nestjs/cli": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", - "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==", + "version": "11.0.21", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.21.tgz", + "integrity": "sha512-F8mV0Sj/zVEouzR3NxBuJy08YHTUOmC5Xdcx3qIIaJWzrm8Vw86CHkhkaPBJ5ewRMHPDCShPmhsfwhpCcjts3A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", - "@angular-devkit/schematics-cli": "19.2.19", + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "@angular-devkit/schematics-cli": "19.2.24", "@inquirer/prompts": "7.10.1", "@nestjs/schematics": "^11.0.1", "ansis": "4.2.0", @@ -2141,13 +2141,13 @@ "cli-table3": "0.6.5", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.1.0", - "glob": "13.0.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.104.1", + "webpack": "5.106.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2157,7 +2157,7 @@ "node": ">= 20.11" }, "peerDependencies": { - "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@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": { @@ -2169,177 +2169,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@nestjs/cli/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@nestjs/cli/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/@nestjs/cli/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nestjs/cli/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nestjs/cli/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nestjs/cli/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/@nestjs/common": { "version": "11.1.17", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", @@ -2372,16 +2201,16 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.17", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz", - "integrity": "sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==", + "version": "11.1.19", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.19.tgz", + "integrity": "sha512-6nJkWa2efrYi+XlU686J9y5L7OvxpLVjT0T/sxRKE7Jvpffiihelup4WSvLvRhdHDjj/5SuoWEwqReXAaaeHmw==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "8.3.0", + "path-to-regexp": "8.4.2", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -2439,14 +2268,14 @@ } }, "node_modules/@nestjs/mapped-types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", - "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz", + "integrity": "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==", "license": "MIT", "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "class-transformer": "^0.4.0 || ^0.5.0", - "class-validator": "^0.13.0 || ^0.14.0", + "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "peerDependenciesMeta": { @@ -2469,15 +2298,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.17", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", - "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", + "version": "11.1.19", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.19.tgz", + "integrity": "sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", "multer": "2.1.1", - "path-to-regexp": "8.3.0", + "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, "funding": { @@ -2490,115 +2319,40 @@ } }, "node_modules/@nestjs/schematics": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", - "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz", + "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.17", - "@angular-devkit/schematics": "19.2.17", - "comment-json": "4.4.1", + "@angular-devkit/core": "19.2.24", + "@angular-devkit/schematics": "19.2.24", + "comment-json": "5.0.0", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, "peerDependencies": { + "prettier": "^3.0.0", "typescript": ">=4.8.2" - } - }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", - "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^4.0.0" }, "peerDependenciesMeta": { - "chokidar": { + "prettier": { "optional": true } } }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", - "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "19.2.17", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@nestjs/schematics/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@nestjs/schematics/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nestjs/schematics/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@nestjs/swagger": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz", - "integrity": "sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==", + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.2.tgz", + "integrity": "sha512-aBihEogDMj/bLEcaqhkvyX/ZVWUw/bmnhKzR0zwUoyGJikvZyaq7rOPYl/H7Lxkkr3c90SJxyuv1AX2UT1WKlw==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", - "@nestjs/mapped-types": "2.1.0", + "@nestjs/mapped-types": "2.1.1", "js-yaml": "4.1.1", - "lodash": "4.17.23", - "path-to-regexp": "8.3.0", - "swagger-ui-dist": "5.31.0" + "lodash": "4.18.1", + "path-to-regexp": "8.4.2", + "swagger-ui-dist": "5.32.4" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", @@ -2672,6 +2426,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nuxt/opencollective": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", @@ -3476,9 +3242,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4108,9 +3874,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -4218,9 +3984,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4476,9 +4242,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4974,14 +4740,13 @@ } }, "node_modules/comment-json": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", - "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", "dev": true, "license": "MIT", "dependencies": { "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", "esprima": "^4.0.1" }, "engines": { @@ -5083,13 +4848,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -5864,9 +5622,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -5879,9 +5637,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", @@ -5890,9 +5648,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6302,18 +6061,18 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6350,9 +6109,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6363,13 +6122,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -6411,9 +6170,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7064,9 +6823,9 @@ } }, "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7225,19 +6984,6 @@ "fsevents": "^2.3.3" } }, - "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-leak-detector": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", @@ -7289,19 +7035,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-mock": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", @@ -7469,9 +7202,9 @@ } }, "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7591,19 +7324,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-validate": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", @@ -7929,9 +7649,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.defaults": { @@ -8147,9 +7867,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -8722,9 +8442,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -8774,9 +8494,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8784,9 +8504,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -8905,9 +8625,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -9866,9 +9586,9 @@ } }, "node_modules/strnum": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.1.tgz", - "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -9943,9 +9663,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", - "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "version": "5.32.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.4.tgz", + "integrity": "sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -10235,19 +9955,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10809,12 +10516,11 @@ } }, "node_modules/webpack": { - "version": "5.105.4", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", - "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "version": "5.106.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz", + "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -10884,7 +10590,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10902,7 +10607,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -10921,7 +10625,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -10935,7 +10638,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10950,7 +10652,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -10960,8 +10661,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", @@ -10969,7 +10669,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -10980,7 +10679,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -10994,7 +10692,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/apps/api/prisma/migrations/20260505120000_tax_service_refactor/migration.sql b/apps/api/prisma/migrations/20260505120000_tax_service_refactor/migration.sql new file mode 100644 index 0000000..39a8c3e --- /dev/null +++ b/apps/api/prisma/migrations/20260505120000_tax_service_refactor/migration.sql @@ -0,0 +1,75 @@ +-- TaxService refactor: TaxNature enum, split tax accounts, tax snapshots, PurchaseInvoice. +-- This migration is additive and backward-compatible. + +-- 1. TaxNature enum +DO $$ BEGIN + CREATE TYPE "TaxNature" AS ENUM ('OUTPUT', 'INPUT'); +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +-- 2. TaxCode: add taxNature, outputAccountId, inputAccountId +ALTER TABLE "TaxCode" ADD COLUMN IF NOT EXISTS "taxNature" "TaxNature" NOT NULL DEFAULT 'OUTPUT'; +ALTER TABLE "TaxCode" ADD COLUMN IF NOT EXISTS "outputAccountId" TEXT; +ALTER TABLE "TaxCode" ADD COLUMN IF NOT EXISTS "inputAccountId" TEXT; + +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'TaxCode_outputAccountId_fkey') THEN + ALTER TABLE "TaxCode" ADD CONSTRAINT "TaxCode_outputAccountId_fkey" + FOREIGN KEY ("outputAccountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'TaxCode_inputAccountId_fkey') THEN + ALTER TABLE "TaxCode" ADD CONSTRAINT "TaxCode_inputAccountId_fkey" + FOREIGN KEY ("inputAccountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS "TaxCode_outputAccountId_idx" ON "TaxCode"("outputAccountId"); +CREATE INDEX IF NOT EXISTS "TaxCode_inputAccountId_idx" ON "TaxCode"("inputAccountId"); + +-- Backfill: copy existing accountId to outputAccountId for OUTPUT tax codes, inputAccountId for INPUT +UPDATE "TaxCode" SET "outputAccountId" = "accountId" WHERE "accountId" IS NOT NULL AND "taxNature" = 'OUTPUT'; +UPDATE "TaxCode" SET "inputAccountId" = "accountId" WHERE "accountId" IS NOT NULL AND "taxNature" = 'INPUT'; + +-- 3. Invoice: add taxRate, taxNature snapshot columns +ALTER TABLE "Invoice" ADD COLUMN IF NOT EXISTS "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "Invoice" ADD COLUMN IF NOT EXISTS "taxNature" "TaxNature" NOT NULL DEFAULT 'OUTPUT'; + +-- 4. PurchaseInvoice table +CREATE TABLE IF NOT EXISTS "PurchaseInvoice" ( + "id" TEXT NOT NULL, + "invoiceNo" TEXT NOT NULL, + "partnerId" TEXT NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "subTotal" DOUBLE PRECISION NOT NULL DEFAULT 0, + "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0, + "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0, + "taxCodeId" TEXT, + "taxNature" "TaxNature" NOT NULL DEFAULT 'INPUT', + "status" TEXT NOT NULL, + "postingStatus" "EntryPostingStatus" NOT NULL DEFAULT 'DRAFT', + "companyId" TEXT NOT NULL, + "dueDate" TIMESTAMP(3), + "issuedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PurchaseInvoice_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX IF NOT EXISTS "PurchaseInvoice_invoiceNo_key" ON "PurchaseInvoice"("invoiceNo"); +CREATE INDEX IF NOT EXISTS "PurchaseInvoice_companyId_status_idx" ON "PurchaseInvoice"("companyId", "status"); +CREATE INDEX IF NOT EXISTS "PurchaseInvoice_partnerId_idx" ON "PurchaseInvoice"("partnerId"); + +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'PurchaseInvoice_partnerId_fkey') THEN + ALTER TABLE "PurchaseInvoice" ADD CONSTRAINT "PurchaseInvoice_partnerId_fkey" + FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'PurchaseInvoice_companyId_fkey') THEN + ALTER TABLE "PurchaseInvoice" ADD CONSTRAINT "PurchaseInvoice_companyId_fkey" + FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'PurchaseInvoice_taxCodeId_fkey') THEN + ALTER TABLE "PurchaseInvoice" ADD CONSTRAINT "PurchaseInvoice_taxCodeId_fkey" + FOREIGN KEY ("taxCodeId") REFERENCES "TaxCode"("id") ON DELETE SET NULL ON UPDATE CASCADE; + END IF; +END $$; diff --git a/apps/api/prisma/migrations/20260505140000_three_way_match/migration.sql b/apps/api/prisma/migrations/20260505140000_three_way_match/migration.sql new file mode 100644 index 0000000..7ec5efc --- /dev/null +++ b/apps/api/prisma/migrations/20260505140000_three_way_match/migration.sql @@ -0,0 +1,42 @@ +-- Three-Way Match: add financial fields to PurchaseOrder/PurchaseOrderLine, add matchStatus to PurchaseInvoice. + +-- 1. ThreeWayMatchStatus enum +DO $$ BEGIN + CREATE TYPE "ThreeWayMatchStatus" AS ENUM ('MATCHED', 'MISMATCH', 'PENDING_REVIEW', 'NOT_APPLICABLE'); +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; + +-- 2. PurchaseOrder: add financial summary columns +ALTER TABLE "PurchaseOrder" ADD COLUMN IF NOT EXISTS "subTotal" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrder" ADD COLUMN IF NOT EXISTS "taxTotal" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrder" ADD COLUMN IF NOT EXISTS "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0; + +-- 3. PurchaseOrderLine: add financial & detail columns +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "lineNo" INTEGER NOT NULL DEFAULT 1; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "description" TEXT; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "taxRate" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "taxAmount" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "subTotal" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "PurchaseOrderLine" ADD COLUMN IF NOT EXISTS "companyId" TEXT; + +-- Make productId and materialId nullable (allow line with just one) +DO $$ BEGIN + ALTER TABLE "PurchaseOrderLine" ALTER COLUMN "productId" DROP NOT NULL; +EXCEPTION + WHEN undefined_column THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "PurchaseOrderLine" ALTER COLUMN "materialId" DROP NOT NULL; +EXCEPTION + WHEN undefined_column THEN NULL; +END $$; + +-- 4. PurchaseInvoice: add matchStatus column +ALTER TABLE "PurchaseInvoice" ADD COLUMN IF NOT EXISTS "matchStatus" "ThreeWayMatchStatus" NOT NULL DEFAULT 'NOT_APPLICABLE'; + +-- 5. Indexes for efficient match lookups +CREATE INDEX IF NOT EXISTS "PurchaseOrderLine_materialId_idx" ON "PurchaseOrderLine"("materialId"); +CREATE INDEX IF NOT EXISTS "GoodsReceiptLine_materialId_idx" ON "GoodsReceiptLine"("materialId"); diff --git a/apps/api/prisma/migrations/20260505160000_purchase_line_account_mapping/migration.sql b/apps/api/prisma/migrations/20260505160000_purchase_line_account_mapping/migration.sql new file mode 100644 index 0000000..8b64b28 --- /dev/null +++ b/apps/api/prisma/migrations/20260505160000_purchase_line_account_mapping/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable: Add accountId to PurchaseInvoiceLine for expense/inventory account mapping +ALTER TABLE "PurchaseInvoiceLine" ADD COLUMN "accountId" TEXT; + +-- AddForeignKey +ALTER TABLE "PurchaseInvoiceLine" ADD CONSTRAINT "PurchaseInvoiceLine_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260505190000_stock_picking_purchase_status/migration.sql b/apps/api/prisma/migrations/20260505190000_stock_picking_purchase_status/migration.sql new file mode 100644 index 0000000..a1ea661 --- /dev/null +++ b/apps/api/prisma/migrations/20260505190000_stock_picking_purchase_status/migration.sql @@ -0,0 +1,15 @@ +-- Extend purchase order status workflow for submit/approval stages. +ALTER TYPE "PurchaseOrderStatus" ADD VALUE IF NOT EXISTS 'SUBMITTED'; +ALTER TYPE "PurchaseOrderStatus" ADD VALUE IF NOT EXISTS 'APPROVED'; + +-- Link stock pickings to source business documents. +ALTER TABLE "StockPicking" ADD COLUMN IF NOT EXISTS "referenceType" TEXT; +ALTER TABLE "StockPicking" ADD COLUMN IF NOT EXISTS "referenceId" TEXT; + +-- Support material-only stock moves and execution tracking. +ALTER TABLE "StockMove" ADD COLUMN IF NOT EXISTS "lineNo" INTEGER NOT NULL DEFAULT 1; +ALTER TABLE "StockMove" ADD COLUMN IF NOT EXISTS "quantityDone" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "StockMove" ALTER COLUMN "productId" DROP NOT NULL; + +-- Relation and lookup indexes used by picking and inventory flows. +CREATE INDEX IF NOT EXISTS "StockPicking_companyId_referenceType_referenceId_idx" ON "StockPicking"("companyId", "referenceType", "referenceId"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index ffb4ea0..6754a88 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -22,7 +22,7 @@ model Company { taxId String? // 税号 address String? // 公司地址 phone String? // 联系电话 - legalPerson String? // 法定代表人 + legalPerson String? // 法定代表�? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -44,7 +44,11 @@ model Company { journals Journal[] journalEntries JournalEntry[] eventDlqRecords EventDlq[] - taxCodes TaxCode[] + taxCodes TaxCode[] + purchaseInvoices PurchaseInvoice[] + purchaseOrders PurchaseOrder[] + goodsReceipts GoodsReceipt[] + stockPickings StockPicking[] } enum PartnerType { @@ -77,9 +81,16 @@ enum JournalType { enum EntryPostingStatus { DRAFT POSTED + REVERSED CANCELLED } + +enum TaxNature { + OUTPUT // 销项税 + INPUT // 进项�? +} + model User { id String @id @default(uuid()) email String @unique @@ -89,16 +100,31 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - companies UserCompanyRole[] - orders Order[] @relation("UserOrders") - FileRecord FileRecord[] - WorkReport WorkReport[] - auditLogs AuditLog[] + companies UserCompanyRole[] + orders Order[] @relation("UserOrders") + FileRecord FileRecord[] + WorkReport WorkReport[] + auditLogs AuditLog[] + refreshTokens RefreshToken[] +} + +model RefreshToken { + id String @id @default(uuid()) + token String @unique + userId String + expiresAt DateTime + revoked Boolean @default(false) + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([token]) } model Role { id String @id @default(uuid()) - name String // "ADMIN", "SALES", "MANAGER" 等 + name String // "ADMIN", "SALES", "MANAGER" �? permissions String[] // JSON or Array 存放详细权限点,例如 ["order:read", "order:write"] users UserCompanyRole[] @@ -145,6 +171,9 @@ model Partner { company Company @relation(fields: [companyId], references: [id]) orders Order[] journalEntryLines JournalEntryLine[] + purchaseInvoices PurchaseInvoice[] + purchaseOrders PurchaseOrder[] + goodsReceipts GoodsReceipt[] @@unique([companyId, code]) @@index([companyId, type]) @@ -154,20 +183,27 @@ model TaxCode { id String @id @default(uuid()) code String name String - rate Float @default(0) + rate Decimal @default(0) @db.Decimal(6, 4) isTaxInclusive Boolean @default(true) isDefault Boolean @default(false) active Boolean @default(true) + taxNature TaxNature @default(OUTPUT) accountId String? + outputAccountId String? + inputAccountId String? companyId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt company Company @relation(fields: [companyId], references: [id]) - account Account? @relation(fields: [accountId], references: [id]) + account Account? @relation(fields: [accountId], references: [id]) + outputAccount Account? @relation("OutputTaxAccount", fields: [outputAccountId], references: [id]) + inputAccount Account? @relation("InputTaxAccount", fields: [inputAccountId], references: [id]) orders Order[] orderItems OrderItem[] invoices Invoice[] + purchaseInvoices PurchaseInvoice[] + purchaseInvoiceLines PurchaseInvoiceLine[] @@unique([companyId, code]) @@index([companyId, active]) @@ -185,11 +221,11 @@ model Order { salesId String // 挂单销售员 companyId String status String // "DRAFT", "PENDING", "IN_PRODUCTION", "SHIPPED", "COMPLETED" - totalAmount Float @default(0) - subTotal Float @default(0) - taxTotal Float @default(0) + totalAmount Decimal @default(0) @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxTotal Decimal @default(0) @db.Decimal(19, 4) taxCodeId String? - aiSummary Json? // 存放 AI 抓取的客户需求 JSON 结构 + aiSummary Json? // 存放 AI 抓取的客户需�?JSON 结构 customAttributes Json? expectedDate DateTime? // 预计交付日期 notes String? // 备注 @@ -209,12 +245,12 @@ model Order { model OrderItem { id String @id @default(uuid()) orderId String - productId String // 关联的产品/模型名称 + productId String // 关联的产�?模型名称 quantity Int - unitPrice Float - totalPrice Float - subTotal Float @default(0) - taxAmount Float @default(0) + unitPrice Decimal @db.Decimal(19, 4) + totalPrice Decimal @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxAmount Decimal @default(0) @db.Decimal(19, 4) taxRate Float @default(0) taxCodeId String? customAttributes Json? @@ -224,12 +260,12 @@ model OrderItem { } // ========================================== -// 智能仓储与物料管理 +// 智能仓储与物料管�? // ========================================== model Warehouse { id String @id @default(uuid()) - name String // "原料库", "成品库", "零件半成品库" + name String // "原料�?, "成品�?, "零件半成品库" type String // "MATERIAL", "FINISHED", "PART" companyId String @@ -245,7 +281,7 @@ model StockLocation { isActive Boolean @default(true) companyId String warehouseId String? - parentId String? // 自引用父库位,支持 华南仓/货架区/A01层 层次结构 + parentId String? // 自引用父库位,支�?华南�?货架�?A01�?层次结构 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -256,6 +292,11 @@ model StockLocation { quants StockQuant[] outgoingTransactions InventoryTransaction[] @relation("StockMoveSourceLocation") incomingTransactions InventoryTransaction[] @relation("StockMoveDestinationLocation") + goodsReceiptLines GoodsReceiptLine[] + sourcePickings StockPicking[] @relation("PickingSourceLocation") + destPickings StockPicking[] @relation("PickingDestLocation") + sourceMoves StockMove[] @relation("PickingSourceLocation") + destMoves StockMove[] @relation("PickingDestLocation") @@unique([companyId, code]) @@index([companyId, warehouseId]) @@ -278,6 +319,10 @@ model Material { inventoryTransactions InventoryTransaction[] products Product[] bomLines BomLine[] + purchaseOrderLines PurchaseOrderLine[] + purchaseInvoiceLines PurchaseInvoiceLine[] + goodsReceiptLines GoodsReceiptLine[] + stockMoves StockMove[] } model ProductCategory { @@ -317,6 +362,9 @@ model Product { category ProductCategory? @relation(fields: [categoryId], references: [id]) material Material? @relation(fields: [materialId], references: [id]) boms Bom[] + purchaseOrderLines PurchaseOrderLine[] + goodsReceiptLines GoodsReceiptLine[] + stockMoves StockMove[] @@unique([companyId, sku]) @@index([companyId, categoryId]) @@ -421,13 +469,15 @@ model InventoryTransaction { batchNo String? operatorId String companyId String - referenceNo String? // 关联单号(订单号/采购号等) + referenceNo String? // 关联单号(订单号/采购号等�? note String? + stockMoveId String? createdAt DateTime @default(now()) - material Material @relation(fields: [materialId], references: [id]) - sourceLocation StockLocation? @relation("StockMoveSourceLocation", fields: [sourceLocationId], references: [id]) - destLocation StockLocation? @relation("StockMoveDestinationLocation", fields: [destLocationId], references: [id]) + material Material @relation(fields: [materialId], references: [id]) + sourceLocation StockLocation? @relation("StockMoveSourceLocation", fields: [sourceLocationId], references: [id]) + stockMove StockMove? @relation(fields: [stockMoveId], references: [id], onDelete: SetNull) + destLocation StockLocation? @relation("StockMoveDestinationLocation", fields: [destLocationId], references: [id]) @@index([companyId, materialId]) @@index([sourceLocationId]) @@ -521,10 +571,12 @@ model Invoice { id String @id @default(uuid()) invoiceNo String @unique orderId String - amount Float - subTotal Float @default(0) - taxAmount Float @default(0) + amount Decimal @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxAmount Decimal @default(0) @db.Decimal(19, 4) taxCodeId String? + taxRate Decimal @default(0) @db.Decimal(6, 4) + taxNature TaxNature @default(OUTPUT) status String // "UNPAID", "PARTIAL", "PAID" postingStatus EntryPostingStatus @default(DRAFT) companyId String @@ -539,13 +591,79 @@ model Invoice { model Payment { id String @id @default(uuid()) invoiceId String - amount Float + amount Decimal @db.Decimal(19, 4) paymentDate DateTime @default(now()) method String // "BANK_TRANSFER", "CASH", "WECHAT", "ALIPAY" invoice Invoice @relation(fields: [invoiceId], references: [id]) } +model PurchaseInvoice { + id String @id @default(uuid()) + invoiceNo String @unique + partnerId String + receiptId String? // 关联入库�? + amount Decimal @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxAmount Decimal @default(0) @db.Decimal(19, 4) + taxRate Decimal @default(0) @db.Decimal(6, 4) + taxCodeId String? + taxNature TaxNature @default(INPUT) + status String // "DRAFT", "UNPAID", "PARTIAL", "PAID" + matchStatus ThreeWayMatchStatus @default(NOT_APPLICABLE) + postingStatus EntryPostingStatus @default(DRAFT) + companyId String + dueDate DateTime? + issuedDate DateTime @default(now()) + notes String? + + partner Partner @relation(fields: [partnerId], references: [id]) + company Company @relation(fields: [companyId], references: [id]) + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + receipt GoodsReceipt? @relation(fields: [receiptId], references: [id]) + lines PurchaseInvoiceLine[] + payments VendorBillPayment[] + + @@index([companyId, status]) + @@index([partnerId]) + @@index([receiptId]) +} + +model PurchaseInvoiceLine { + id String @id @default(uuid()) + invoiceId String + materialId String + quantity Float + unitPrice Decimal @db.Decimal(19, 4) + lineTotal Decimal @default(0) @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxAmount Decimal @default(0) @db.Decimal(19, 4) + taxRate Decimal @default(0) @db.Decimal(6, 4) + taxCodeId String? + accountId String? + description String? + + invoice PurchaseInvoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) + material Material @relation(fields: [materialId], references: [id]) + taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) + account Account? @relation(fields: [accountId], references: [id]) + + @@index([invoiceId]) +} + +model VendorBillPayment { + id String @id @default(uuid()) + invoiceId String + amount Decimal @db.Decimal(19, 4) + paymentDate DateTime @default(now()) + method String // "BANK_TRANSFER", "CASH", "CHECK" + notes String? + + invoice PurchaseInvoice @relation(fields: [invoiceId], references: [id]) + + @@index([invoiceId]) +} + model Account { id String @id @default(uuid()) code String @@ -561,7 +679,10 @@ model Account { children Account[] @relation("AccountTree") company Company @relation(fields: [companyId], references: [id]) lines JournalEntryLine[] - taxCodes TaxCode[] + purchaseInvoiceLines PurchaseInvoiceLine[] + taxCodes TaxCode[] + outputTaxCodes TaxCode[] @relation("OutputTaxAccount") + inputTaxCodes TaxCode[] @relation("InputTaxAccount") @@unique([companyId, code]) @@index([companyId, parentId]) @@ -585,18 +706,19 @@ model Journal { } model JournalEntry { - id String @id @default(uuid()) - entryNo String - date DateTime - ref String? - description String? - journalId String - companyId String - postingStatus EntryPostingStatus @default(DRAFT) - createdBy String? - postedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + entryNo String + date DateTime + ref String? + description String? + journalId String + companyId String + postingStatus EntryPostingStatus @default(DRAFT) + reversedEntryId String? // 冲销凭证指向原凭证 ID + createdBy String? + postedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt journal Journal @relation(fields: [journalId], references: [id]) company Company @relation(fields: [companyId], references: [id]) @@ -604,6 +726,7 @@ model JournalEntry { @@unique([companyId, entryNo]) @@index([companyId, journalId, date]) + @@index([reversedEntryId]) } model JournalEntryLine { @@ -612,8 +735,8 @@ model JournalEntryLine { lineNo Int accountId String partnerId String? - debit Float @default(0) - credit Float @default(0) + debit Decimal @default(0) @db.Decimal(19, 4) + credit Decimal @default(0) @db.Decimal(19, 4) memo String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -648,6 +771,196 @@ model EventDlq { @@index([eventName, idempotencyKey]) } + +// ========================================== +// 采购与入库模�? +// ========================================== + +enum PurchaseOrderStatus { + DRAFT + SUBMITTED + APPROVED + CONFIRMED + PARTIALLY_RECEIVED + RECEIVED + CLOSED + CANCELLED +} + +enum GoodsReceiptStatus { + DRAFT + CONFIRMED + CLOSED + CANCELLED +} + +enum PickingType { + INBOUND + OUTBOUND + INTERNAL_TRANSFER +} + +enum PickingStatus { + DRAFT + CONFIRMED + DONE + CANCELLED +} + +enum MoveStatus { + DRAFT + CONFIRMED + DONE + CANCELLED +} + +enum ThreeWayMatchStatus { + MATCHED + MISMATCH + PENDING_REVIEW + NOT_APPLICABLE +} + + +model PurchaseOrder { + id String @id @default(uuid()) + orderNo String @unique + partnerId String + status PurchaseOrderStatus @default(DRAFT) + orderDate DateTime @default(now()) + expectedDate DateTime? + notes String? + subTotal Decimal @default(0) @db.Decimal(19, 4) + taxTotal Decimal @default(0) @db.Decimal(19, 4) + totalAmount Decimal @default(0) @db.Decimal(19, 4) + companyId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + partner Partner @relation(fields: [partnerId], references: [id]) + lines PurchaseOrderLine[] + receipts GoodsReceipt[] + + @@index([companyId, status]) + @@index([partnerId]) +} + +model PurchaseOrderLine { + id String @id @default(uuid()) + orderId String + lineNo Int @default(1) + productId String? + materialId String? + description String? + quantity Float + receivedQuantity Float @default(0) + unitPrice Decimal @db.Decimal(19, 4) + taxRate Decimal @default(0) @db.Decimal(6, 4) + taxAmount Decimal @default(0) @db.Decimal(19, 4) + subTotal Decimal @default(0) @db.Decimal(19, 4) + totalAmount Decimal @default(0) @db.Decimal(19, 4) + companyId String + uom String @default("pcs") + + order PurchaseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade) + product Product? @relation(fields: [productId], references: [id]) + material Material? @relation(fields: [materialId], references: [id]) + + @@index([orderId]) +} + +model GoodsReceipt { + id String @id @default(uuid()) + receiptNo String @unique + orderId String? + partnerId String + status GoodsReceiptStatus @default(DRAFT) + receiptDate DateTime @default(now()) + notes String? + companyId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + partner Partner @relation(fields: [partnerId], references: [id]) + order PurchaseOrder? @relation(fields: [orderId], references: [id]) + lines GoodsReceiptLine[] + invoices PurchaseInvoice[] + + @@index([companyId, status]) + @@index([partnerId]) +} + +model GoodsReceiptLine { + id String @id @default(uuid()) + receiptId String + productId String + materialId String + locationId String + quantity Float + batchNo String? + + receipt GoodsReceipt @relation(fields: [receiptId], references: [id], onDelete: Cascade) + product Product @relation(fields: [productId], references: [id]) + material Material @relation(fields: [materialId], references: [id]) + location StockLocation @relation(fields: [locationId], references: [id]) + + @@index([receiptId]) +} + +model StockPicking { + id String @id @default(uuid()) + pickingNo String @unique + type PickingType + status PickingStatus @default(DRAFT) + scheduledDate DateTime? + completedDate DateTime? + sourceLocationId String? + destLocationId String? + referenceType String? + referenceId String? + notes String? + companyId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + company Company @relation(fields: [companyId], references: [id]) + sourceLocation StockLocation? @relation("PickingSourceLocation", fields: [sourceLocationId], references: [id]) + destLocation StockLocation? @relation("PickingDestLocation", fields: [destLocationId], references: [id]) + moves StockMove[] + + @@index([companyId, status]) + @@index([companyId, referenceType, referenceId]) +} + +model StockMove { + id String @id @default(uuid()) + pickingId String + lineNo Int @default(1) + productId String? + materialId String + quantity Float + quantityDone Float @default(0) + sourceLocationId String? + destLocationId String? + status MoveStatus @default(DRAFT) + batchNo String? + companyId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + picking StockPicking @relation(fields: [pickingId], references: [id], onDelete: Cascade) + product Product? @relation(fields: [productId], references: [id]) + material Material @relation(fields: [materialId], references: [id]) + sourceLocation StockLocation? @relation("PickingSourceLocation", fields: [sourceLocationId], references: [id]) + destLocation StockLocation? @relation("PickingDestLocation", fields: [destLocationId], references: [id]) + inventoryTransactions InventoryTransaction[] + + @@index([pickingId]) + @@index([companyId, materialId]) +} + model AuditLog { id String @id @default(uuid()) userId String diff --git a/apps/api/scripts/export-prisma-ddl-context.ts b/apps/api/scripts/export-prisma-ddl-context.ts index 03a0051..d9580c7 100644 --- a/apps/api/scripts/export-prisma-ddl-context.ts +++ b/apps/api/scripts/export-prisma-ddl-context.ts @@ -39,6 +39,12 @@ function main() { 'JournalEntry', 'JournalEntryLine', 'Account', + 'PurchaseOrder', + 'PurchaseOrderLine', + 'GoodsReceipt', + 'GoodsReceiptLine', + 'StockPicking', + 'StockMove', ].includes(model.name), ); diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index d820294..3abe61c 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -13,6 +13,8 @@ import { ProductionModule } from './production/production.module'; import { FinanceModule } from './finance/finance.module'; import { LoggerMiddleware } from './core/middlewares/logger.middleware'; import { DepartmentsModule } from './departments/departments.module'; +import { PurchaseOrdersModule } from './purchase-orders/purchase-orders.module'; +import { GoodsReceiptsModule } from './goods-receipts/goods-receipts.module'; import { AppCacheModule } from './core/cache/cache.module'; import { CrudModule } from './core/crud/crud.module'; import { MetadataModule } from './core/metadata/metadata.module'; @@ -21,6 +23,7 @@ import { AIModule } from './core/ai/ai.module'; import { AuditModule } from './core/audit/audit.module'; import { TenantContextMiddleware } from './core/middlewares/tenant-context.middleware'; import { KyselyModule } from './core/prisma/kysely.module'; +import { TaxModule } from './core/tax/tax.module'; @Module({ imports: [ @@ -28,6 +31,7 @@ import { KyselyModule } from './core/prisma/kysely.module'; AppCacheModule, KyselyModule, PrismaModule, + TaxModule, MetadataModule, WorkflowModule, AuditModule, @@ -42,6 +46,8 @@ import { KyselyModule } from './core/prisma/kysely.module'; ProductionModule, FinanceModule, DepartmentsModule, + PurchaseOrdersModule, + GoodsReceiptsModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 8e71a3e..a931624 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -5,7 +5,9 @@ import { HttpCode, HttpStatus, UnauthorizedException, + Headers, } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { AuthService } from './auth.service'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { LoginDto } from './dto/login.dto'; @@ -14,11 +16,14 @@ import { RegisterDto } from './dto/register.dto'; @ApiTags('认证 (Auth)') @Controller('auth') export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly jwtService: JwtService, + ) {} @HttpCode(HttpStatus.OK) @Post('login') - @ApiOperation({ summary: '用户登录' }) + @ApiOperation({ summary: '用户登录(返回 accessToken + refreshToken)' }) async login(@Body() signInDto: LoginDto) { const user = await this.authService.validateUser( signInDto.email, @@ -39,4 +44,42 @@ export class AuthController { signUpDto.name, ); } + + @HttpCode(HttpStatus.OK) + @Post('refresh') + @ApiOperation({ summary: '刷新 access token(需要 refreshToken)' }) + async refresh(@Body() body: { refreshToken: string }) { + if (!body.refreshToken) { + throw new UnauthorizedException('缺少 refreshToken'); + } + return this.authService.refresh(body.refreshToken); + } + + @HttpCode(HttpStatus.OK) + @Post('logout') + @ApiOperation({ summary: '登出(撤销 refreshToken)' }) + async logout( + @Headers('authorization') authorization: string, + @Body() body: { refreshToken?: string }, + ) { + if (!authorization) { + throw new UnauthorizedException('未登录'); + } + const token = authorization.replace('Bearer ', ''); + try { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const decoded = this.jwtService.decode(token) as Record< + string, + unknown + > | null; + const sub = decoded?.sub; + if (typeof sub !== 'string' || !sub) { + throw new UnauthorizedException('无效的 token'); + } + await this.authService.logout(sub, body.refreshToken); + return { message: '已登出' }; + } catch { + throw new UnauthorizedException('登出失败'); + } + } } diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 54b0f17..f992d93 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,19 +1,33 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { randomBytes } from 'node:crypto'; import { UsersService } from '../users/users.service'; import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '../prisma/prisma.service'; import * as bcrypt from 'bcrypt'; import type { AuthUserProfile, AuthenticatedUser, } from '../core/http/request.types'; +const ACCESS_TOKEN_EXPIRY = '15m'; +const REFRESH_TOKEN_EXPIRY_DAYS = 30; + type AuthResult = { accessToken: string; + refreshToken: string; + accessTokenExpiresIn: number; user: AuthUserProfile; companies: Array<{ id: string; name: string; - role: string; + role: { + name: string; + permissions: string[]; + }; }>; }; @@ -22,6 +36,7 @@ export class AuthService { constructor( private usersService: UsersService, private jwtService: JwtService, + private prisma: PrismaService, ) {} async validateUser( @@ -40,10 +55,81 @@ export class AuthService { return null; } - login(user: AuthenticatedUser): AuthResult { + async login(user: AuthenticatedUser): Promise { + const payload = { email: user.email, sub: user.id }; + const accessToken = this.jwtService.sign(payload, { + expiresIn: ACCESS_TOKEN_EXPIRY, + }); + const refreshToken = await this.createRefreshToken(user.id); + + return { + accessToken, + refreshToken, + accessTokenExpiresIn: 15 * 60, // 15 minutes in seconds + user: { + id: user.id, + email: user.email, + name: user.name, + }, + companies: user.companies.map((membership) => ({ + id: membership.company.id, + name: membership.company.name, + role: { + name: membership.role.name, + permissions: membership.role.permissions, + }, + })), + }; + } + + async refresh(refreshToken: string): Promise { + const stored = await this.prisma.refreshToken.findUnique({ + where: { token: refreshToken }, + include: { + user: { + include: { + companies: { + include: { company: true, role: true }, + }, + }, + }, + }, + }); + + if (!stored) { + throw new UnauthorizedException('无效的 refresh token'); + } + + if (stored.revoked) { + // Token reuse detected — revoke all tokens for this user (security) + await this.prisma.refreshToken.updateMany({ + where: { userId: stored.userId }, + data: { revoked: true }, + }); + throw new UnauthorizedException('Refresh token 已被撤销,请重新登录'); + } + + if (stored.expiresAt < new Date()) { + throw new UnauthorizedException('Refresh token 已过期,请重新登录'); + } + + // Revoke the old refresh token (rotation) + await this.prisma.refreshToken.update({ + where: { id: stored.id }, + data: { revoked: true }, + }); + + const user = stored.user; const payload = { email: user.email, sub: user.id }; + const accessToken = this.jwtService.sign(payload, { + expiresIn: ACCESS_TOKEN_EXPIRY, + }); + const newRefreshToken = await this.createRefreshToken(user.id); + return { - accessToken: this.jwtService.sign(payload), + accessToken, + refreshToken: newRefreshToken, + accessTokenExpiresIn: 15 * 60, user: { id: user.id, email: user.email, @@ -52,11 +138,30 @@ export class AuthService { companies: user.companies.map((membership) => ({ id: membership.company.id, name: membership.company.name, - role: membership.role.name, + role: { + name: membership.role.name, + permissions: membership.role.permissions, + }, })), }; } + async logout(userId: string, refreshToken?: string): Promise { + if (refreshToken) { + // Revoke specific token + await this.prisma.refreshToken.updateMany({ + where: { token: refreshToken, userId }, + data: { revoked: true }, + }); + } else { + // Revoke all tokens for user + await this.prisma.refreshToken.updateMany({ + where: { userId }, + data: { revoked: true }, + }); + } + } + async register( email: string, pass: string, @@ -64,7 +169,7 @@ export class AuthService { ): Promise { const existing = await this.usersService.findByEmail(email); if (existing) { - throw new UnauthorizedException('Email already exists'); + throw new ConflictException('Email already exists'); } const user = await this.usersService.createUser(email, pass, name); @@ -75,4 +180,20 @@ export class AuthService { companies: [], }); } + + private async createRefreshToken(userId: string): Promise { + const token = randomBytes(40).toString('hex'); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + REFRESH_TOKEN_EXPIRY_DAYS); + + await this.prisma.refreshToken.create({ + data: { + token, + userId, + expiresAt, + }, + }); + + return token; + } } diff --git a/apps/api/src/core/ai/ai.module.ts b/apps/api/src/core/ai/ai.module.ts index 4f66851..76cd913 100644 --- a/apps/api/src/core/ai/ai.module.ts +++ b/apps/api/src/core/ai/ai.module.ts @@ -5,11 +5,12 @@ import { CrudModule } from '../crud/crud.module'; import { MetadataModule } from '../metadata/metadata.module'; import { WorkflowModule } from '../workflow/workflow.module'; import { LlmAdapterService } from './llm-adapter.service'; +import { SafeChat2SqlService } from './safe-chat2sql.service'; @Module({ imports: [CrudModule, MetadataModule, WorkflowModule], controllers: [AIController], - providers: [AIService, LlmAdapterService], - exports: [AIService], + providers: [AIService, LlmAdapterService, SafeChat2SqlService], + exports: [AIService, SafeChat2SqlService], }) export class AIModule {} diff --git a/apps/api/src/core/ai/ai.service.ts b/apps/api/src/core/ai/ai.service.ts index 87609f4..3548717 100644 --- a/apps/api/src/core/ai/ai.service.ts +++ b/apps/api/src/core/ai/ai.service.ts @@ -4,6 +4,7 @@ import { CrudService } from '../crud/crud.service'; import { MetadataService } from '../metadata/metadata.service'; import { WorkflowService } from '../workflow/workflow.service'; import { LlmAdapterService } from './llm-adapter.service'; +import { SafeChat2SqlService } from './safe-chat2sql.service'; export interface AIToolSchema { name: string; @@ -39,6 +40,7 @@ export class AIService { private readonly metadataService: MetadataService, private readonly workflowService: WorkflowService, private readonly llmAdapterService: LlmAdapterService, + private readonly safeChat2SqlService: SafeChat2SqlService, ) {} private toSafeText(value: unknown): string { @@ -292,29 +294,8 @@ export class AIService { } async chat2sql(input: string, companyId: string) { - const schemaContext = this.buildReadSchemaContext(); - const sql = await this.llmAdapterService.resolveReadSql( - input, - schemaContext, - ); - - if (!sql) { - throw new BadRequestException('未生成可执行查询,请重试更具体的问题'); - } - - const checkedSql = this.validateReadOnlySql(sql); - const rows: Array> = - await this.prisma.$queryRawUnsafe(checkedSql, companyId); - - const chartSuggestion = this.suggestChart(rows); - return { - type: 'table', - title: 'Chat2SQL 查询结果', - sql: checkedSql, - rows, - chartSuggestion, - rowCount: rows.length, - }; + // 安全实现:使用 Prisma ORM 结构化查询,不使用 $queryRawUnsafe + return this.safeChat2SqlService.execute(input, companyId); } async parseDocumentDraft( diff --git a/apps/api/src/core/ai/safe-chat2sql.service.ts b/apps/api/src/core/ai/safe-chat2sql.service.ts new file mode 100644 index 0000000..a384e9a --- /dev/null +++ b/apps/api/src/core/ai/safe-chat2sql.service.ts @@ -0,0 +1,471 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; + +/** + * 安全 Chat2SQL 服务 + * + * 不使用 $queryRawUnsafe,而是将自然语言问题映射到预定义的安全 Prisma 查询。 + * 所有查询强制注入 companyId 过滤,避免 SQL 注入和跨租户越权。 + */ + +interface StructuredQuery { + model: string; + aggregation?: 'count' | 'sum' | 'avg'; + aggregateField?: string; + groupBy?: string; + filters?: Record; + orderBy?: Record; + take?: number; + select?: string[]; +} + +export interface QueryResultRow { + [key: string]: unknown; +} + +// 白名单:允许查询的模型及其安全字段 +const ALLOWED_MODELS: Record< + string, + { + table: string; + groupableFields: string[]; + aggregatableFields: string[]; + filterableFields: string[]; + } +> = { + order: { + table: 'order', + groupableFields: ['status', 'createdAt'], + aggregatableFields: ['totalAmount', 'subTotal', 'taxTotal'], + filterableFields: ['status', 'createdAt', 'partnerId'], + }, + invoice: { + table: 'invoice', + groupableFields: ['status', 'postingStatus', 'issuedDate'], + aggregatableFields: ['amount', 'subTotal', 'taxAmount'], + filterableFields: ['status', 'postingStatus', 'issuedDate'], + }, + partner: { + table: 'partner', + groupableFields: ['type'], + aggregatableFields: [], + filterableFields: ['type', 'isActive'], + }, + material: { + table: 'material', + groupableFields: ['category'], + aggregatableFields: ['unitPrice', 'minStock'], + filterableFields: ['category'], + }, + inventoryTransaction: { + table: 'inventoryTransaction', + groupableFields: ['type', 'createdAt'], + aggregatableFields: ['quantity'], + filterableFields: ['type', 'createdAt', 'materialId'], + }, + purchaseOrder: { + table: 'purchaseOrder', + groupableFields: ['status', 'orderDate'], + aggregatableFields: ['totalAmount', 'subTotal', 'taxTotal'], + filterableFields: ['status', 'orderDate', 'partnerId'], + }, + goodsReceipt: { + table: 'goodsReceipt', + groupableFields: ['status', 'receiptDate'], + aggregatableFields: [], + filterableFields: ['status', 'receiptDate', 'partnerId'], + }, +}; + +@Injectable() +export class SafeChat2SqlService { + private readonly logger = new Logger(SafeChat2SqlService.name); + + constructor(private readonly prisma: PrismaService) {} + + /** + * 将自然语言问题映射到安全的结构化查询并执行。 + */ + async execute( + question: string, + companyId: string, + ): Promise<{ + type: string; + title: string; + rows: QueryResultRow[]; + rowCount: number; + queryDescription: string; + chartSuggestion: { chartType: string; xKey?: string; yKey?: string }; + }> { + const structured = this.parseQuestion(question); + this.validateQuery(structured); + + const rows = await this.executeStructuredQuery(structured, companyId); + const chartSuggestion = this.suggestChart(rows); + + return { + type: 'table', + title: `查询结果:${structured.model}`, + rows, + rowCount: rows.length, + queryDescription: this.describeQuery(structured), + chartSuggestion, + }; + } + + /** + * 将自然语言解析为结构化查询。 + * 使用关键词匹配而非 LLM,避免生成不安全 SQL。 + */ + private parseQuestion(question: string): StructuredQuery { + const q = question.toLowerCase(); + + // 识别模型 + const model = this.detectModel(q); + + // 识别聚合类型 + const aggregation = this.detectAggregation(q); + + // 识别分组 + const groupBy = this.detectGroupBy(q, model); + + // 识别时间过滤 + const filters = this.detectTimeFilter(q); + + // 识别排序和限制 + const take = this.detectLimit(q); + + return { + model, + aggregation: aggregation ?? undefined, + aggregateField: aggregation + ? this.detectAggregateField(q, model) + : undefined, + groupBy: groupBy ?? undefined, + filters: Object.keys(filters).length > 0 ? filters : undefined, + orderBy: { createdAt: 'desc' }, + take: take ?? 20, + }; + } + + private detectModel(q: string): string { + if (q.includes('采购订单') || q.includes('purchase')) + return 'purchaseOrder'; + if (q.includes('入库') || q.includes('收货')) return 'goodsReceipt'; + if (q.includes('发票') || q.includes('invoice')) return 'invoice'; + if (q.includes('库存') || q.includes('transaction')) + return 'inventoryTransaction'; + if ( + q.includes('客户') || + q.includes('供应商') || + q.includes('伙伴') || + q.includes('partner') + ) + return 'partner'; + if (q.includes('物料') || q.includes('material')) return 'material'; + return 'order'; + } + + private detectAggregation(q: string): 'count' | 'sum' | 'avg' | null { + if ( + q.includes('多少') || + q.includes('数量') || + q.includes('count') || + q.includes('几个') + ) + return 'count'; + if ( + q.includes('总') || + q.includes('合计') || + q.includes('sum') || + q.includes('金额') + ) + return 'sum'; + if (q.includes('平均') || q.includes('avg')) return 'avg'; + return null; + } + + private detectGroupBy(q: string, model: string): string | null { + const config = ALLOWED_MODELS[model]; + if (!config) return null; + + for (const field of config.groupableFields) { + // 中文关键词映射 + const keywords = this.getFieldKeywords(field); + if (keywords.some((kw) => q.includes(kw))) { + return field; + } + } + + // 如果有聚合但没有明确分组,按 status 分组 + if ( + (q.includes('多少') || q.includes('总') || q.includes('合计')) && + config.groupableFields.includes('status') + ) { + return 'status'; + } + + return null; + } + + private getFieldKeywords(field: string): string[] { + const map: Record = { + status: ['状态', 'status'], + type: ['类型', 'type'], + category: ['分类', 'category'], + createdAt: ['时间', '日期', '日', '月', '年'], + issuedDate: ['开票日期', '开票时间'], + orderDate: ['下单日期', '下单时间'], + receiptDate: ['收货日期', '收货时间'], + postingStatus: ['过账状态'], + }; + return map[field] ?? [field]; + } + + private detectAggregateField(q: string, model: string): string | undefined { + const config = ALLOWED_MODELS[model]; + if (!config?.aggregatableFields.length) return undefined; + + for (const field of config.aggregatableFields) { + const keywords = this.getFieldKeywords(field); + if (keywords.some((kw) => q.includes(kw))) { + return field; + } + } + + // 默认使用第一个可聚合字段 + return config.aggregatableFields[0]; + } + + private detectTimeFilter(q: string): Record { + const now = new Date(); + + if (q.includes('今天') || q.includes('today')) { + const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + return { createdAt: { gte: start } }; + } + + if (q.includes('本周') || q.includes('这周')) { + const start = new Date(now); + start.setDate(now.getDate() - now.getDay()); + start.setHours(0, 0, 0, 0); + return { createdAt: { gte: start } }; + } + + if (q.includes('本月') || q.includes('这个月')) { + const start = new Date(now.getFullYear(), now.getMonth(), 1); + return { createdAt: { gte: start } }; + } + + if (q.includes('30天') || q.includes('一个月')) { + const start = new Date(now); + start.setDate(now.getDate() - 30); + return { createdAt: { gte: start } }; + } + + if (q.includes('90天') || q.includes('三个月') || q.includes('一季度')) { + const start = new Date(now); + start.setDate(now.getDate() - 90); + return { createdAt: { gte: start } }; + } + + // 默认最近 7 天 + if (q.includes('最近') || q.includes('recent')) { + const days = this.extractDays(q) ?? 7; + const start = new Date(now); + start.setDate(now.getDate() - days); + return { createdAt: { gte: start } }; + } + + return {}; + } + + private extractDays(q: string): number | null { + const match = q.match(/(\d+)\s*[天日]/); + if (match) { + const n = parseInt(match[1], 10); + if (n > 0 && n <= 365) return n; + } + return null; + } + + private detectLimit(q: string): number | null { + const match = q.match(/(?:前|top|最多)\s*(\d+)/i); + if (match) { + const n = parseInt(match[1], 10); + if (n > 0 && n <= 100) return n; + } + return null; + } + + private validateQuery(query: StructuredQuery): void { + const config = ALLOWED_MODELS[query.model]; + if (!config) { + throw new BadRequestException(`不支持查询模型: ${query.model}`); + } + + if (query.groupBy && !config.groupableFields.includes(query.groupBy)) { + throw new BadRequestException(`不允许按字段 ${query.groupBy} 分组`); + } + + if ( + query.aggregateField && + !config.aggregatableFields.includes(query.aggregateField) + ) { + throw new BadRequestException(`不允许聚合字段 ${query.aggregateField}`); + } + } + + /** + * 通过 Prisma ORM 执行结构化查询,完全避免 SQL 注入。 + */ + private async executeStructuredQuery( + query: StructuredQuery, + companyId: string, + ): Promise { + const delegate = (this.prisma as unknown as Record)[ + query.model + ] as { + findMany?: (args: Record) => Promise; + groupBy?: (args: Record) => Promise; + count?: (args: Record) => Promise; + }; + + if (!delegate) { + throw new BadRequestException(`Prisma 模型 ${query.model} 不可用`); + } + + const baseWhere: Record = { + ...query.filters, + companyId, + }; + + // 聚合 + 分组查询 + if (query.aggregation && query.groupBy) { + if (!delegate.groupBy) { + throw new BadRequestException(`模型 ${query.model} 不支持 groupBy`); + } + + const groupByArgs: Record = { + by: [query.groupBy], + where: baseWhere, + orderBy: { [query.groupBy]: 'asc' as const }, + }; + + if (query.aggregation === 'count') { + groupByArgs._count = { _all: true }; + } else if (query.aggregateField) { + groupByArgs[`_${query.aggregation}`] = { + [query.aggregateField]: true, + }; + } + + const grouped = (await delegate.groupBy(groupByArgs)) as Array< + Record + >; + + return grouped.map((row) => { + const result: QueryResultRow = { + [query.groupBy!]: row[query.groupBy!], + }; + + if (query.aggregation === 'count') { + result.count = (row._count as Record)?._all ?? 0; + } else if (query.aggregateField) { + const aggKey = `_${query.aggregation}`; + result[`${query.aggregation}_${query.aggregateField}`] = + (row[aggKey] as Record)?.[query.aggregateField] ?? + 0; + } + + return result; + }); + } + + // 简单计数 + if (query.aggregation === 'count' && !query.groupBy) { + if (!delegate.count) return []; + const total = await delegate.count({ where: baseWhere }); + return [{ count: total }]; + } + + // 普通列表查询 + if (!delegate.findMany) return []; + + const findManyArgs: Record = { + where: baseWhere, + take: query.take ?? 20, + }; + + if (query.orderBy) { + findManyArgs.orderBy = query.orderBy; + } + + if (query.select?.length) { + findManyArgs.select = query.select.reduce>( + (acc, field) => { + acc[field] = true; + return acc; + }, + {}, + ); + } + + const rows = await delegate.findMany(findManyArgs); + return rows as QueryResultRow[]; + } + + private describeQuery(query: StructuredQuery): string { + const parts: string[] = [`查询 ${query.model}`]; + + if (query.aggregation) { + const aggLabel = + query.aggregation === 'count' + ? '计数' + : query.aggregation === 'sum' + ? '求和' + : '平均值'; + parts.push(`聚合: ${aggLabel}`); + if (query.aggregateField) { + parts.push(`字段: ${query.aggregateField}`); + } + } + + if (query.groupBy) { + parts.push(`分组: ${query.groupBy}`); + } + + if (query.filters) { + parts.push(`过滤: ${JSON.stringify(query.filters)}`); + } + + parts.push(`限制: ${query.take ?? 20} 条`); + + return parts.join(' | '); + } + + private suggestChart(rows: QueryResultRow[]): { + chartType: string; + xKey?: string; + yKey?: string; + } { + if (!rows.length) { + return { chartType: 'table' }; + } + + const first = rows[0]; + const keys = Object.keys(first); + const numericKey = keys.find((key) => typeof first[key] === 'number'); + const categoryKey = keys.find((key) => typeof first[key] === 'string'); + + if (!numericKey || !categoryKey) { + return { chartType: 'table' }; + } + + return { + chartType: 'bar', + xKey: categoryKey, + yKey: numericKey, + }; + } +} diff --git a/apps/api/src/core/cache/cache.module.ts b/apps/api/src/core/cache/cache.module.ts index a044b78..f591a63 100644 --- a/apps/api/src/core/cache/cache.module.ts +++ b/apps/api/src/core/cache/cache.module.ts @@ -4,11 +4,8 @@ import { CacheModule } from '@nestjs/cache-manager'; @Module({ imports: [ CacheModule.register({ - store: 'ioredis', - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379'), - ttl: 60, // 60秒缓存 isGlobal: true, + ttl: 60_000, }), ], }) diff --git a/apps/api/src/core/crud/crud-query-validator.spec.ts b/apps/api/src/core/crud/crud-query-validator.spec.ts index 31538c7..a56aa73 100644 --- a/apps/api/src/core/crud/crud-query-validator.spec.ts +++ b/apps/api/src/core/crud/crud-query-validator.spec.ts @@ -224,4 +224,4 @@ describe('getDmmfModel', () => { it('null 输入应返回 undefined', () => { expect(getDmmfModel(null, 'Order')).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/apps/api/src/core/crud/crud-query-validator.ts b/apps/api/src/core/crud/crud-query-validator.ts index e47dfae..d536f4f 100644 --- a/apps/api/src/core/crud/crud-query-validator.ts +++ b/apps/api/src/core/crud/crud-query-validator.ts @@ -137,7 +137,7 @@ export function sanitizeFilter( if (PRISMA_LOGICAL_OPERATORS.has(key)) { const value = filter[key]; if (Array.isArray(value)) { - result[key] = value.map((item) => + result[key] = (value as unknown[]).map((item) => typeof item === 'object' && item !== null ? sanitizeFilter(item as Record, modelMeta) : item, diff --git a/apps/api/src/core/crud/crud.service.spec.ts b/apps/api/src/core/crud/crud.service.spec.ts index e242184..168e7e0 100644 --- a/apps/api/src/core/crud/crud.service.spec.ts +++ b/apps/api/src/core/crud/crud.service.spec.ts @@ -1,4 +1,8 @@ -import { ForbiddenException, NotFoundException, BadRequestException } from '@nestjs/common'; +import { + ForbiddenException, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; import { CrudService } from './crud.service'; // --------------------------------------------------------------------------- @@ -16,7 +20,9 @@ function createMockDelegate() { }; } -function createMockPrisma(modelDelegate: ReturnType) { +function createMockPrisma( + modelDelegate: ReturnType, +) { return { order: modelDelegate, partner: modelDelegate, @@ -27,10 +33,25 @@ function createMockPrisma(modelDelegate: ReturnType) fields: [ { name: 'id', kind: 'scalar', type: 'String', isList: false }, { name: 'orderNo', kind: 'scalar', type: 'String', isList: false }, - { name: 'partnerId', kind: 'scalar', type: 'String', isList: false }, - { name: 'companyId', kind: 'scalar', type: 'String', isList: false }, + { + name: 'partnerId', + kind: 'scalar', + type: 'String', + isList: false, + }, + { + name: 'companyId', + kind: 'scalar', + type: 'String', + isList: false, + }, { name: 'status', kind: 'scalar', type: 'String', isList: false }, - { name: 'totalAmount', kind: 'scalar', type: 'Float', isList: false }, + { + name: 'totalAmount', + kind: 'scalar', + type: 'Float', + isList: false, + }, { name: 'partner', kind: 'object', type: 'Partner', isList: false }, { name: 'items', kind: 'object', type: 'OrderItem', isList: true }, ], @@ -40,7 +61,12 @@ function createMockPrisma(modelDelegate: ReturnType) fields: [ { name: 'id', kind: 'scalar', type: 'String', isList: false }, { name: 'name', kind: 'scalar', type: 'String', isList: false }, - { name: 'companyId', kind: 'scalar', type: 'String', isList: false }, + { + name: 'companyId', + kind: 'scalar', + type: 'String', + isList: false, + }, { name: 'company', kind: 'object', type: 'Company', isList: false }, ], }, @@ -49,7 +75,9 @@ function createMockPrisma(modelDelegate: ReturnType) }; } -function createMockMetadataService(companyScoped = new Set(['order', 'partner'])) { +function createMockMetadataService( + companyScoped = new Set(['order', 'partner']), +) { return { isCompanyScoped: jest.fn((m: string) => companyScoped.has(m)), getSchema: jest.fn().mockRejectedValue(new Error('not found')), @@ -58,7 +86,9 @@ function createMockMetadataService(companyScoped = new Set(['order', 'partner']) } function createMockHooksService() { - return { execute: jest.fn((_e: string, ctx: unknown) => Promise.resolve(ctx)) }; + return { + execute: jest.fn((_e: string, ctx: unknown) => Promise.resolve(ctx)), + }; } function createMockAuditService() { @@ -72,7 +102,10 @@ function buildService(deps?: { companyScopedModels?: Set }) { const hooks = createMockHooksService(); const audit = createMockAuditService(); const service = new CrudService( - prisma as never, metadata as never, hooks as never, audit as never, + prisma as never, + metadata as never, + hooks as never, + audit as never, ); return { service, delegate, metadata }; } @@ -110,7 +143,11 @@ describe('CrudService – 租户隔离: list()', () => { const { service } = buildService(); await expect( - service.list('order', { filter: JSON.stringify({ companyId: 'c2' }) }, 'c1'), + service.list( + 'order', + { filter: JSON.stringify({ companyId: 'c2' }) }, + 'c1', + ), ).rejects.toBeInstanceOf(ForbiddenException); }); }); @@ -224,9 +261,9 @@ describe('CrudService – 租户隔离: remove()', () => { const { service, delegate } = buildService(); delegate.findFirst.mockResolvedValue(null); - await expect( - service.remove('order', 'r1', 'c1'), - ).rejects.toBeInstanceOf(NotFoundException); + await expect(service.remove('order', 'r1', 'c1')).rejects.toBeInstanceOf( + NotFoundException, + ); }); }); @@ -242,4 +279,4 @@ describe('CrudService – 未知模型', () => { BadRequestException, ); }); -}); \ No newline at end of file +}); diff --git a/apps/api/src/core/crud/crud.service.ts b/apps/api/src/core/crud/crud.service.ts index b3716e2..f55a123 100644 --- a/apps/api/src/core/crud/crud.service.ts +++ b/apps/api/src/core/crud/crud.service.ts @@ -55,7 +55,8 @@ export class CrudService { const normalizedModelName = this.normalizeModelName(modelName); const model = this.resolveModel(normalizedModelName); const modelMeta = this.getDmmfModelMeta(normalizedModelName); - const rawFilter = parseJsonParam>(query.filter) ?? {}; + const rawFilter = + parseJsonParam>(query.filter) ?? {}; const filter = sanitizeFilter(rawFilter, modelMeta); const where = this.applyCompanyScope( normalizedModelName, @@ -71,7 +72,9 @@ export class CrudService { const fields = parseJsonParam(query.fields, { allowCommaList: true }); const select = normalizeSelect(fields); - const rawInclude = select ? undefined : parseJsonParam>(query.include); + const rawInclude = select + ? undefined + : parseJsonParam>(query.include); const include = sanitizeInclude(rawInclude, modelMeta); const schema = await this.getSchemaIfExists(normalizedModelName, companyId); const rawOrderBy = @@ -112,7 +115,9 @@ export class CrudService { const fields = parseJsonParam(query.fields, { allowCommaList: true }); const select = normalizeSelect(fields); const findOneModelMeta = this.getDmmfModelMeta(normalizedModelName); - const rawInclude = select ? undefined : parseJsonParam>(query.include); + const rawInclude = select + ? undefined + : parseJsonParam>(query.include); const include = sanitizeInclude(rawInclude, findOneModelMeta); const record = await model.findFirst({ where, select, include }); diff --git a/apps/api/src/core/decorators/require-permissions.decorator.ts b/apps/api/src/core/decorators/require-permissions.decorator.ts new file mode 100644 index 0000000..ac6470e --- /dev/null +++ b/apps/api/src/core/decorators/require-permissions.decorator.ts @@ -0,0 +1,13 @@ +import { SetMetadata } from '@nestjs/common'; + +/** + * 权限点声明装饰器。 + * 用法:@RequirePermissions('purchase:write', 'purchase:cancel') + * 表示当前端点需要用户拥有列出的所有权限点之一。 + * + * 权限点命名约定: : + * 例如: purchase:read, purchase:write, finance:post, finance:reverse + */ +export const PERMISSIONS_KEY = 'required_permissions'; +export const RequirePermissions = (...permissions: string[]) => + SetMetadata(PERMISSIONS_KEY, permissions); diff --git a/apps/api/src/core/guards/permissions.guard.spec.ts b/apps/api/src/core/guards/permissions.guard.spec.ts new file mode 100644 index 0000000..9fa1526 --- /dev/null +++ b/apps/api/src/core/guards/permissions.guard.spec.ts @@ -0,0 +1,87 @@ +import { ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PermissionsGuard } from './permissions.guard'; + +function mockContext( + userRole: { name: string; permissions?: string[] } | null, + userId = 'u1', +): ExecutionContext { + return { + switchToHttp: () => ({ + getRequest: () => ({ + user: { id: userId, email: 'test@test.com' }, + userRole, + }), + }), + getHandler: () => () => undefined, + getClass: () => class TestController {}, + } as unknown as ExecutionContext; +} + +describe('PermissionsGuard', () => { + let guard: PermissionsGuard; + let reflector: Reflector; + + beforeEach(() => { + reflector = new Reflector(); + guard = new PermissionsGuard(reflector); + }); + + it('should allow access when no permissions are declared', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined); + const ctx = mockContext({ name: 'VIEWER' }); + expect(guard.canActivate(ctx)).toBe(true); + }); + + it('should allow access when permissions list is empty', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue([]); + const ctx = mockContext({ name: 'VIEWER' }); + expect(guard.canActivate(ctx)).toBe(true); + }); + + it('should auto-grant ADMIN role full access', () => { + jest + .spyOn(reflector, 'getAllAndOverride') + .mockReturnValue(['finance:reverse']); + const ctx = mockContext({ name: 'ADMIN' }); + expect(guard.canActivate(ctx)).toBe(true); + }); + + it('should allow access when user has required permission', () => { + jest + .spyOn(reflector, 'getAllAndOverride') + .mockReturnValue(['purchase:write', 'purchase:cancel']); + const ctx = mockContext({ + name: 'MANAGER', + permissions: ['purchase:read', 'purchase:write'], + }); + expect(guard.canActivate(ctx)).toBe(true); + }); + + it('should deny access when user lacks required permission', () => { + jest + .spyOn(reflector, 'getAllAndOverride') + .mockReturnValue(['finance:reverse']); + const ctx = mockContext({ + name: 'VIEWER', + permissions: ['purchase:read'], + }); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + + it('should deny access when user has no permissions array', () => { + jest + .spyOn(reflector, 'getAllAndOverride') + .mockReturnValue(['purchase:write']); + const ctx = mockContext({ name: 'EMPTY_ROLE' }); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); + + it('should deny access when userRole is null', () => { + jest + .spyOn(reflector, 'getAllAndOverride') + .mockReturnValue(['purchase:write']); + const ctx = mockContext(null); + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); + }); +}); diff --git a/apps/api/src/core/guards/permissions.guard.ts b/apps/api/src/core/guards/permissions.guard.ts new file mode 100644 index 0000000..2b974ab --- /dev/null +++ b/apps/api/src/core/guards/permissions.guard.ts @@ -0,0 +1,74 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, + Logger, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { PERMISSIONS_KEY } from '../decorators/require-permissions.decorator'; +import type { RequestWithAuth } from '../http/request.types'; + +/** + * 权限守卫 —— 配合 @RequirePermissions() 装饰器使用。 + * + * 工作原理: + * 1. TenantGuard 已将当前用户的 role(含 permissions[])绑定到 request.userRole。 + * 2. 本守卫读取 @RequirePermissions('xxx') 声明的权限点列表。 + * 3. 检查 request.userRole.permissions 是否包含声明的任一权限点。 + * 4. 特殊规则: ADMIN 角色自动获得所有权限(全局通过)。 + * 5. 若端点未使用 @RequirePermissions(),本守卫默认放行。 + * + * 使用顺序: @UseGuards(JwtAuthGuard, TenantGuard, PermissionsGuard) + */ +@Injectable() +export class PermissionsGuard implements CanActivate { + private readonly logger = new Logger(PermissionsGuard.name); + + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredPermissions = this.reflector.getAllAndOverride< + string[] | undefined + >(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]); + + // 未声明 @RequirePermissions() 的端点,默认放行 + if (!requiredPermissions || requiredPermissions.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const role = request.userRole; + + if (!role) { + this.logger.warn( + `权限拒绝: 用户 ${request.user?.id} 无角色信息 (应先经过 TenantGuard)`, + ); + throw new ForbiddenException('无角色信息,无法验证权限'); + } + + // ADMIN 角色自动全权限 + if (role.name === 'ADMIN') { + return true; + } + + const userPermissions: string[] = + (role as unknown as { permissions?: string[] }).permissions ?? []; + + // 检查用户是否拥有所需权限的任一项 + const hasPermission = requiredPermissions.some((perm) => + userPermissions.includes(perm), + ); + + if (!hasPermission) { + this.logger.warn( + `权限拒绝: 用户 ${request.user?.id} 角色 ${role.name} 缺少权限 [${requiredPermissions.join(', ')}]`, + ); + throw new ForbiddenException( + `权限不足,需要以下权限之一: ${requiredPermissions.join(', ')}`, + ); + } + + return true; + } +} diff --git a/apps/api/src/core/guards/throttler-behind-proxy.guard.ts b/apps/api/src/core/guards/throttler-behind-proxy.guard.ts index 2977546..2bf0cde 100644 --- a/apps/api/src/core/guards/throttler-behind-proxy.guard.ts +++ b/apps/api/src/core/guards/throttler-behind-proxy.guard.ts @@ -13,25 +13,23 @@ import { Request } from 'express'; */ @Injectable() export class ThrottlerBehindProxyGuard extends ThrottlerGuard { - protected async getTracker(req: Record): Promise { - const request = req as Request; - + protected getTracker(request: Request): Promise { // X-Forwarded-For 可能包含多个 IP(经过多级代理),取第一个即真实客户端 IP const forwarded = request.headers['x-forwarded-for']; if (forwarded) { const ip = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0].trim(); - return ip; + return Promise.resolve(ip); } // 其次尝试 X-Real-IP(Nginx 常用配置) const realIp = request.headers['x-real-ip']; if (realIp) { - return Array.isArray(realIp) ? realIp[0] : realIp; + return Promise.resolve(Array.isArray(realIp) ? realIp[0] : realIp); } // 兜底使用 Express 解析出的 IP - return request.ip ?? 'unknown'; + return Promise.resolve(request.ip ?? 'unknown'); } } diff --git a/apps/api/src/core/http/request.types.ts b/apps/api/src/core/http/request.types.ts index 09f86e8..08d2c58 100644 --- a/apps/api/src/core/http/request.types.ts +++ b/apps/api/src/core/http/request.types.ts @@ -17,6 +17,7 @@ export interface AuthCompanyRef { export interface AuthRoleRef { name: string; + permissions: string[]; } export interface AuthCompanyMembership { diff --git a/apps/api/src/core/metadata/metadata.service.ts b/apps/api/src/core/metadata/metadata.service.ts index 662a5a0..4d55b47 100644 --- a/apps/api/src/core/metadata/metadata.service.ts +++ b/apps/api/src/core/metadata/metadata.service.ts @@ -358,14 +358,13 @@ export class MetadataService { companyScoped: true, fields: [ { name: 'name', label: '部门名称', type: 'string', required: true }, - { name: 'code', label: '部门编码', type: 'string' }, ], views: { - form: { fields: ['name', 'code'] }, + form: { fields: ['name'] }, list: { - columns: ['name', 'code'], - defaultSort: { createdAt: 'desc' }, - searchFields: ['name', 'code'], + columns: ['name'], + defaultSort: { id: 'desc' }, + searchFields: ['name'], }, }, }, @@ -400,14 +399,13 @@ export class MetadataService { relationField: 'role', }, }, - { name: 'createdAt', label: '加入时间', type: 'date' }, ], views: { form: { fields: ['userId', 'roleId'] }, list: { - columns: ['userId', 'roleId', 'createdAt'], - defaultSort: { createdAt: 'desc' }, - searchFields: ['userId', 'roleId'], + columns: ['userId', 'roleId'], + defaultSort: { id: 'desc' }, + searchFields: [], }, }, }, @@ -606,6 +604,520 @@ export class MetadataService { }, }, ], + [ + 'goodsReceipt', + { + model: 'goodsReceipt', + label: '采购收货', + description: '采购入库管理与质检记录。', + companyScoped: true, + fields: [ + { + name: 'receiptNo', + label: '收货单号', + type: 'string', + required: true, + }, + { + name: 'partnerId', + label: '供应商', + type: 'reference', + required: true, + reference: { + model: 'partner', + labelField: 'name', + valueField: 'id', + relationField: 'partner', + }, + }, + { + name: 'orderId', + label: '关联采购单', + type: 'reference', + reference: { + model: 'purchaseOrder', + labelField: 'orderNo', + valueField: 'id', + relationField: 'order', + }, + }, + { + name: 'status', + label: '状态', + type: 'select', + options: [ + { label: '草稿', value: 'DRAFT' }, + { label: '待检验', value: 'PENDING_INSPECTION' }, + { label: '已确认', value: 'CONFIRMED' }, + { label: '已冲销', value: 'REVERSED' }, + ], + }, + { name: 'receiptDate', label: '收货日期', type: 'date' }, + { name: 'notes', label: '备注', type: 'text' }, + { + name: 'lines', + label: '收货明细', + type: 'subtable', + subtable: { + fields: [ + { + name: 'productId', + label: '产品', + type: 'reference', + required: true, + reference: { + model: 'product', + labelField: 'name', + valueField: 'id', + relationField: 'product', + }, + }, + { + name: 'materialId', + label: '物料', + type: 'reference', + required: true, + reference: { + model: 'material', + labelField: 'name', + valueField: 'id', + relationField: 'material', + }, + }, + { + name: 'locationId', + label: '入库库位', + type: 'reference', + required: true, + reference: { + model: 'stockLocation', + labelField: 'name', + valueField: 'id', + relationField: 'location', + }, + }, + { + name: 'quantity', + label: '收货数量', + type: 'number', + required: true, + }, + { name: 'batchNo', label: '批次号', type: 'string' }, + ], + }, + }, + ], + views: { + form: { + fields: [ + 'receiptNo', + 'partnerId', + 'orderId', + 'status', + 'receiptDate', + 'notes', + 'lines', + ], + }, + list: { + columns: [ + 'receiptNo', + 'partnerId', + 'orderId', + 'status', + 'receiptDate', + ], + defaultSort: { createdAt: 'desc' }, + searchFields: ['receiptNo', 'notes'], + }, + kanban: { + statusField: 'status', + columns: [ + { value: 'DRAFT', label: '草稿', color: 'bg-slate-50' }, + { + value: 'PENDING_INSPECTION', + label: '待检验', + color: 'bg-amber-50', + }, + { + value: 'CONFIRMED', + label: '已确认入库', + color: 'bg-emerald-50', + }, + { value: 'REVERSED', label: '已冲销', color: 'bg-rose-50' }, + ], + }, + }, + actions: [ + { + name: 'confirm', + label: '确认入库', + style: 'primary', + endpoint: '/v1/purchase/goods-receipts/:id/confirm', + method: 'POST', + visibility: "eval:doc.status==='DRAFT'", + requiresPermission: ['inventory:write'], + }, + { + name: 'reverse', + label: '冲销入库单', + style: 'danger', + endpoint: '/v1/purchase/goods-receipts/:id/reverse', + method: 'POST', + visibility: "eval:doc.status==='CONFIRMED'", + requiresPermission: ['inventory:write'], + prompt: '请输入冲销原因:', + }, + ], + }, + ], + [ + 'purchaseOrder', + { + model: 'purchaseOrder', + label: '采购订单', + description: '采购订单管理,支持从草稿到收货的全流程。', + companyScoped: true, + fields: [ + { name: 'orderNo', label: '订单号', type: 'string', required: true }, + { + name: 'partnerId', + label: '供应商', + type: 'reference', + required: true, + reference: { + model: 'partner', + labelField: 'name', + valueField: 'id', + relationField: 'partner', + }, + }, + { + name: 'status', + label: '状态', + type: 'select', + options: [ + { label: '草稿', value: 'DRAFT' }, + { label: '已提交', value: 'SUBMITTED' }, + { label: '已审批', value: 'APPROVED' }, + { label: '已确认', value: 'CONFIRMED' }, + { label: '部分收货', value: 'PARTIALLY_RECEIVED' }, + { label: '已收货', value: 'RECEIVED' }, + { label: '已关闭', value: 'CLOSED' }, + { label: '已取消', value: 'CANCELLED' }, + ], + }, + { name: 'orderDate', label: '下单日期', type: 'date' }, + { name: 'expectedDate', label: '预计到货', type: 'date' }, + { name: 'subTotal', label: '未税金额', type: 'number' }, + { name: 'taxTotal', label: '税额', type: 'number' }, + { name: 'totalAmount', label: '总金额', type: 'number' }, + { name: 'notes', label: '备注', type: 'text' }, + { + name: 'lines', + label: '采购明细', + type: 'subtable', + subtable: { + fields: [ + { + name: 'materialId', + label: '物料', + type: 'reference', + required: true, + reference: { + model: 'material', + labelField: 'name', + valueField: 'id', + relationField: 'material', + }, + }, + { + name: 'quantity', + label: '数量', + type: 'number', + required: true, + }, + { + name: 'unitPrice', + label: '单价', + type: 'number', + required: true, + }, + { name: 'taxRate', label: '税率(%)', type: 'number' }, + ], + }, + }, + ], + views: { + form: { + fields: [ + 'orderNo', + 'partnerId', + 'status', + 'orderDate', + 'expectedDate', + 'totalAmount', + 'notes', + 'lines', + ], + }, + list: { + columns: [ + 'orderNo', + 'partnerId', + 'status', + 'orderDate', + 'subTotal', + 'taxTotal', + 'totalAmount', + 'expectedDate', + ], + defaultSort: { createdAt: 'desc' }, + searchFields: ['orderNo', 'notes'], + }, + kanban: { + statusField: 'status', + columns: [ + { value: 'DRAFT', label: '草稿', color: 'bg-slate-50' }, + { value: 'SUBMITTED', label: '已提交', color: 'bg-amber-50' }, + { value: 'APPROVED', label: '已审批', color: 'bg-sky-50' }, + { value: 'CONFIRMED', label: '已确认', color: 'bg-indigo-50' }, + { + value: 'PARTIALLY_RECEIVED', + label: '部分收货', + color: 'bg-orange-50', + }, + { value: 'RECEIVED', label: '已收货', color: 'bg-emerald-50' }, + { value: 'CLOSED', label: '已关闭', color: 'bg-gray-100' }, + { value: 'CANCELLED', label: '已取消', color: 'bg-rose-50' }, + ], + }, + }, + }, + ], + [ + 'purchaseInvoice', + { + model: 'purchaseInvoice', + label: '采购发票', + description: '供应商发票与付款管理。', + companyScoped: true, + fields: [ + { name: 'invoiceNo', label: '发票号', type: 'string' }, + { + name: 'partnerId', + label: '供应商', + type: 'reference', + reference: { + model: 'partner', + labelField: 'name', + valueField: 'id', + relationField: 'partner', + }, + }, + { + name: 'taxCodeId', + label: '税码', + type: 'reference', + reference: { + model: 'taxCode', + labelField: 'name', + valueField: 'id', + relationField: 'taxCode', + }, + }, + { + name: 'taxNature', + label: '税务属性', + type: 'select', + options: [ + { label: '进项税', value: 'INPUT' }, + { label: '销项税', value: 'OUTPUT' }, + ], + }, + { name: 'status', label: '状态', type: 'string' }, + { name: 'subTotal', label: '未税金额', type: 'number' }, + { name: 'taxAmount', label: '税额', type: 'number' }, + { name: 'amount', label: '金额', type: 'number' }, + { name: 'dueDate', label: '到期日', type: 'date' }, + ], + views: { + form: { + fields: [ + 'invoiceNo', + 'partnerId', + 'taxCodeId', + 'taxNature', + 'status', + 'amount', + 'dueDate', + ], + }, + list: { + columns: [ + 'invoiceNo', + 'partnerId', + 'taxCodeId', + 'taxNature', + 'status', + 'subTotal', + 'taxAmount', + 'amount', + 'dueDate', + ], + defaultSort: { createdAt: 'desc' }, + searchFields: ['invoiceNo', 'status'], + }, + kanban: { + statusField: 'status', + columns: [ + { value: 'UNPAID', label: '未付款', color: 'bg-rose-50' }, + { value: 'PARTIAL', label: '部分付款', color: 'bg-amber-50' }, + { value: 'PAID', label: '已付款', color: 'bg-emerald-50' }, + ], + }, + }, + actions: [ + { + name: 'confirm', + label: '确认发票', + style: 'primary', + endpoint: '/v1/finance/vendor-bills/:id/confirm', + method: 'POST', + visibility: "eval:doc.status==='DRAFT'", + requiresPermission: ['finance:write'], + }, + { + name: 'match', + label: '三单匹配', + style: 'default', + endpoint: '/v1/finance/vendor-bills/:id/match', + method: 'POST', + visibility: "eval:doc.status==='UNPAID'", + requiresPermission: ['finance:write'], + }, + { + name: 'post', + label: '过账凭证', + style: 'primary', + endpoint: '/v1/finance/vendor-bills/:id/post', + method: 'POST', + visibility: "eval:doc.status==='UNPAID'", + requiresPermission: ['finance:approve'], + }, + ], + }, + ], + [ + 'journalEntry', + { + model: 'journalEntry', + label: '财务凭证', + description: '会计分录凭证。', + companyScoped: true, + fields: [ + { name: 'entryNo', label: '凭证号', type: 'string', required: true }, + { name: 'date', label: '凭证日期', type: 'date', required: true }, + { + name: 'journalId', + label: '账簿', + type: 'reference', + required: true, + reference: { + model: 'journal', + labelField: 'name', + valueField: 'id', + relationField: 'journal', + }, + }, + { name: 'reference', label: '参考/单据号', type: 'string' }, + { + name: 'postingStatus', + label: '状态', + type: 'select', + options: [ + { label: '草稿', value: 'DRAFT' }, + { label: '已过账', value: 'POSTED' }, + { label: '已冲销', value: 'REVERSED' }, + { label: '已取消', value: 'CANCELLED' }, + ], + }, + { name: 'totalDebit', label: '总借方', type: 'number' }, + { name: 'totalCredit', label: '总贷方', type: 'number' }, + { + name: 'lines', + label: '分录明细', + type: 'subtable', + subtable: { + fields: [ + { + name: 'accountId', + label: '科目', + type: 'reference', + required: true, + reference: { + model: 'account', + labelField: 'name', + valueField: 'id', + relationField: 'account', + }, + }, + { name: 'debit', label: '借方金额', type: 'number' }, + { name: 'credit', label: '贷方金额', type: 'number' }, + { name: 'description', label: '摘要', type: 'string' }, + ], + }, + }, + ], + views: { + form: { + fields: [ + 'entryNo', + 'date', + 'journalId', + 'reference', + 'postingStatus', + 'totalDebit', + 'totalCredit', + 'lines', + ], + }, + list: { + columns: [ + 'entryNo', + 'date', + 'journalId', + 'reference', + 'postingStatus', + 'totalDebit', + 'totalCredit', + ], + defaultSort: { date: 'desc' }, + searchFields: ['entryNo', 'reference'], + }, + kanban: { + statusField: 'postingStatus', + columns: [ + { value: 'DRAFT', label: '草稿', color: 'bg-slate-50' }, + { value: 'POSTED', label: '已过账', color: 'bg-emerald-50' }, + { value: 'REVERSED', label: '已冲销', color: 'bg-rose-50' }, + ], + }, + }, + actions: [ + { + name: 'reverse', + label: '冲销凭证', + style: 'danger', + endpoint: '/v1/finance/journal-entries/:id/reverse', + method: 'POST', + visibility: "eval:doc.postingStatus==='POSTED'", + requiresPermission: ['finance:reverse', 'finance:write'], + prompt: '请输入冲销原因:', + }, + ], + }, + ], ]); async listSchemas(companyId?: string) { diff --git a/apps/api/src/core/metadata/schema.types.ts b/apps/api/src/core/metadata/schema.types.ts index 9ba6df3..e97bdd1 100644 --- a/apps/api/src/core/metadata/schema.types.ts +++ b/apps/api/src/core/metadata/schema.types.ts @@ -5,7 +5,8 @@ export type UiFieldType = | 'date' | 'select' | 'text' - | 'reference'; + | 'reference' + | 'subtable'; export interface UiFieldOption { label: string; @@ -19,6 +20,12 @@ export interface UiFieldReference { relationField?: string; } +export interface UiSubtableConfig { + fields: UiFieldSchema[]; + minRows?: number; + maxRows?: number; +} + export interface UiFieldSchema { name: string; label: string; @@ -27,6 +34,7 @@ export interface UiFieldSchema { custom?: boolean; options?: UiFieldOption[]; reference?: UiFieldReference; + subtable?: UiSubtableConfig; } export interface UiFormSection { @@ -56,6 +64,18 @@ export interface UiKanbanView { columns: UiKanbanColumn[]; } +export interface UiAction { + name: string; + label: string; + icon?: string; + style?: 'primary' | 'danger' | 'default'; + endpoint: string; + method?: 'POST' | 'PUT' | 'DELETE'; + visibility?: string; + requiresPermission?: string[]; + prompt?: string; +} + export interface UiSchema { model: string; label: string; @@ -67,4 +87,5 @@ export interface UiSchema { list: UiListView; kanban?: UiKanbanView; }; + actions?: UiAction[]; } diff --git a/apps/api/src/core/tax/tax.module.ts b/apps/api/src/core/tax/tax.module.ts new file mode 100644 index 0000000..99a6f4a --- /dev/null +++ b/apps/api/src/core/tax/tax.module.ts @@ -0,0 +1,14 @@ +import { Module, Global } from '@nestjs/common'; +import { PrismaModule } from '../../prisma/prisma.module'; +import { TaxService } from './tax.service'; + +/** + * 税码服务全局模块 —— 任何需要计算税率的模块只需注入 TaxService。 + */ +@Global() +@Module({ + imports: [PrismaModule], + providers: [TaxService], + exports: [TaxService], +}) +export class TaxModule {} diff --git a/apps/api/src/core/tax/tax.service.spec.ts b/apps/api/src/core/tax/tax.service.spec.ts new file mode 100644 index 0000000..1a789b4 --- /dev/null +++ b/apps/api/src/core/tax/tax.service.spec.ts @@ -0,0 +1,192 @@ +import { TaxNature } from '@prisma/client'; +import { TaxService } from './tax.service'; + +type MockPrisma = { + taxCode: { findFirst: jest.Mock }; + auditLog: { create: jest.Mock }; +}; + +describe('TaxService', () => { + const prisma: MockPrisma = { + taxCode: { findFirst: jest.fn() }, + auditLog: { create: jest.fn() }, + }; + + let service: TaxService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new TaxService( + prisma as unknown as ConstructorParameters[0], + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('round2', () => { + it('should round to 2 decimal places', () => { + expect(service.round2(1.005)).toBe(1.01); + expect(service.round2(0)).toBe(0); + expect(service.round2(123.456)).toBe(123.46); + }); + }); + + describe('calcTaxFromTotal', () => { + it('should calculate subTotal and tax from inclusive total', () => { + const result = service.calcTaxFromTotal(113, 0.13); + expect(result.subTotal).toBe(100); + expect(result.taxAmount).toBe(13); + expect(result.total).toBe(113); + expect(result.taxRate).toBe(0.13); + }); + + it('should handle zero taxRate', () => { + const result = service.calcTaxFromTotal(100, 0); + expect(result.subTotal).toBe(100); + expect(result.taxAmount).toBe(0); + }); + + it('should clamp taxRate to [0,1]', () => { + const result = service.calcTaxFromTotal(100, 2); + expect(result.taxRate).toBe(1); + }); + }); + + describe('calcTaxBreakdown', () => { + it('should calculate inclusive tax', () => { + const result = service.calcTaxBreakdown(113, 0.13, true); + expect(result.subTotal).toBe(100); + expect(result.taxAmount).toBe(13); + expect(result.total).toBe(113); + }); + + it('should calculate exclusive tax', () => { + const result = service.calcTaxBreakdown(100, 0.13, false); + expect(result.subTotal).toBe(100); + expect(result.taxAmount).toBe(13); + expect(result.total).toBe(113); + }); + }); + + describe('resolveTaxCode', () => { + it('should return explicit tax code when found', async () => { + prisma.taxCode.findFirst.mockResolvedValue({ + id: 'tc1', + code: 'VAT_13', + name: '增值税13%', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: 'acc1', + inputAccountId: null, + accountId: 'acc1', + }); + + const result = await service.resolveTaxCode('c1', 'tc1'); + expect(result.id).toBe('tc1'); + expect(result.isFallback).toBe(false); + expect(result.rate).toBe(0.13); + }); + + it('should throw when explicit taxCodeId not found', async () => { + prisma.taxCode.findFirst.mockResolvedValue(null); + await expect(service.resolveTaxCode('c1', 'nonexistent')).rejects.toThrow( + '税码不存在或已停用', + ); + }); + + it('should return default tax code when no explicit id', async () => { + prisma.taxCode.findFirst.mockResolvedValue({ + id: 'tc-default', + code: 'VAT_13', + name: '增值税13%', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: null, + inputAccountId: null, + accountId: null, + }); + + const result = await service.resolveTaxCode('c1'); + expect(result.id).toBe('tc-default'); + expect(result.isFallback).toBe(false); + }); + + it('should fallback to 13% and audit when no default configured', async () => { + prisma.taxCode.findFirst.mockResolvedValue(null); + prisma.auditLog.create.mockResolvedValue({}); + + const result = await service.resolveTaxCode('c1', null, { + operatorId: 'u1', + entity: 'Order', + entityId: 'o1', + }); + + expect(result.id).toBeNull(); + expect(result.rate).toBe(0.13); + expect(result.isFallback).toBe(true); + expect(prisma.auditLog.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + action: 'TAX_FALLBACK', + entity: 'Order', + entityId: 'o1', + }), + }), + ); + }); + }); + + describe('getTaxAccountId', () => { + it('should return outputAccountId for OUTPUT nature', () => { + const resolved = { + id: 'tc1', + code: '', + name: '', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: 'out-acc', + inputAccountId: 'in-acc', + accountId: 'legacy-acc', + isFallback: false, + }; + expect(service.getTaxAccountId(resolved)).toBe('out-acc'); + }); + + it('should return inputAccountId for INPUT nature', () => { + const resolved = { + id: 'tc1', + code: '', + name: '', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.INPUT, + outputAccountId: 'out-acc', + inputAccountId: 'in-acc', + accountId: 'legacy-acc', + isFallback: false, + }; + expect(service.getTaxAccountId(resolved, TaxNature.INPUT)).toBe('in-acc'); + }); + + it('should fallback to accountId when specific field is null', () => { + const resolved = { + id: 'tc1', + code: '', + name: '', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: null, + inputAccountId: null, + accountId: 'legacy-acc', + isFallback: false, + }; + expect(service.getTaxAccountId(resolved)).toBe('legacy-acc'); + }); + }); +}); diff --git a/apps/api/src/core/tax/tax.service.ts b/apps/api/src/core/tax/tax.service.ts new file mode 100644 index 0000000..c2b65d2 --- /dev/null +++ b/apps/api/src/core/tax/tax.service.ts @@ -0,0 +1,216 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { TaxNature } from '@prisma/client'; +import { PrismaService } from '../../prisma/prisma.service'; + +/** 税码解析结果,附带 fallback 标记 */ +export interface ResolvedTaxCode { + id: string | null; + code: string; + name: string; + rate: number; + isTaxInclusive: boolean; + taxNature: TaxNature; + outputAccountId: string | null; + inputAccountId: string | null; + /** legacy 兼容字段 */ + accountId: string | null; + isFallback: boolean; +} + +/** 含税/未税计算结果 */ +export interface TaxBreakdown { + subTotal: number; + taxAmount: number; + total: number; + taxRate: number; + taxNature: TaxNature; +} + +/** + * 统一税码服务 —— 唯一的税率解析 & 价税计算入口。 + * + * 设计原则: + * 1. 所有模块(Order / Invoice / PurchaseInvoice)调用此服务解析税码和计算税额。 + * 2. fallback 到 13% 硬编码税率时写入 AuditLog 并返回 isFallback=true。 + * 3. 过账(posting)时禁止重新按当前税率覆盖历史快照金额 —— 由调用方保证。 + */ +@Injectable() +export class TaxService { + private readonly logger = new Logger(TaxService.name); + + constructor(private readonly prisma: PrismaService) {} + + // ─── 数值精度工具 ─────────────────────────────────── + + round2(value: number): number { + return Math.round((value + Number.EPSILON) * 100) / 100; + } + + // ─── 税码解析 ─────────────────────────────────────── + + /** + * 解析税码。 + * 优先使用显式 taxCodeId → 回退到公司默认税码 → 硬编码 13% 兜底。 + * 兜底时写入 AuditLog(仅当 auditContext 存在时)。 + */ + async resolveTaxCode( + companyId: string, + taxCodeId?: string | null, + auditContext?: { operatorId: string; entity: string; entityId: string }, + ): Promise { + // 1. 显式指定 + if (taxCodeId) { + const taxCode = await this.prisma.taxCode.findFirst({ + where: { id: taxCodeId, companyId, active: true }, + }); + if (!taxCode) { + throw new BadRequestException('税码不存在或已停用,请确认税码选择'); + } + return this.toResolved(taxCode, false); + } + + // 2. 公司默认税码 + const defaultTaxCode = await this.prisma.taxCode.findFirst({ + where: { companyId, isDefault: true, active: true }, + orderBy: { updatedAt: 'desc' }, + }); + if (defaultTaxCode) { + return this.toResolved(defaultTaxCode, false); + } + + // 3. 硬编码兜底 + this.logger.warn( + `未配置默认税码,使用 13% 默认税率兜底: companyId=${companyId}`, + ); + + if (auditContext) { + await this.prisma.auditLog.create({ + data: { + userId: auditContext.operatorId, + action: 'TAX_FALLBACK', + entity: auditContext.entity, + entityId: auditContext.entityId, + details: { + reason: '未配置默认税码', + fallbackRate: 0.13, + companyId, + }, + companyId, + }, + }); + } + + return { + id: null, + code: 'FALLBACK_13', + name: '默认税率 13%', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: null, + inputAccountId: null, + accountId: null, + isFallback: true, + }; + } + + /** + * 根据 taxNature 获取对应的税务科目 ID: + * OUTPUT → outputAccountId,INPUT → inputAccountId。 + * 如果对应字段为空,回退到 accountId(兼容旧数据)。 + */ + getTaxAccountId( + resolved: ResolvedTaxCode, + taxNature?: TaxNature, + ): string | null { + const nature = taxNature ?? resolved.taxNature; + if (nature === TaxNature.INPUT) { + return resolved.inputAccountId ?? resolved.accountId; + } + return resolved.outputAccountId ?? resolved.accountId; + } + + // ─── 价税计算 ─────────────────────────────────────── + + /** + * 含税/未税统一计算。 + * @param baseAmount 基础金额(含税金额或未税金额,取决于 isTaxInclusive) + * @param taxRate 税率 (0-1) + * @param isTaxInclusive true=含税价,false=未税价 + */ + calcTaxBreakdown( + baseAmount: number, + taxRate: number, + isTaxInclusive: boolean, + taxNature: TaxNature = TaxNature.OUTPUT, + ): TaxBreakdown { + const safeRate = Math.max(0, Math.min(1, Number(taxRate ?? 0))); + + if (isTaxInclusive) { + const subTotal = this.round2(baseAmount / (1 + safeRate)); + const taxAmount = this.round2(baseAmount - subTotal); + return { + subTotal, + taxAmount, + total: this.round2(baseAmount), + taxRate: safeRate, + taxNature, + }; + } + + const subTotal = this.round2(baseAmount); + const taxAmount = this.round2(subTotal * safeRate); + const total = this.round2(subTotal + taxAmount); + return { subTotal, taxAmount, total, taxRate: safeRate, taxNature }; + } + + /** + * 从含税总价反算 —— 兼容 FinanceService 的旧接口。 + */ + calcTaxFromTotal( + total: number, + taxRate: number, + taxNature: TaxNature = TaxNature.OUTPUT, + ): TaxBreakdown { + const safeRate = Math.max(0, Math.min(1, Number(taxRate ?? 0))); + const subTotal = this.round2(total / (1 + safeRate)); + const taxAmount = this.round2(total - subTotal); + return { + subTotal, + taxAmount, + total: this.round2(total), + taxRate: safeRate, + taxNature, + }; + } + + // ─── 内部方法 ────────────────────────────────────── + + private toResolved( + tc: { + id: string; + code: string; + name: string; + rate: number | { toNumber(): number }; + isTaxInclusive: boolean; + taxNature: TaxNature; + outputAccountId: string | null; + inputAccountId: string | null; + accountId: string | null; + }, + isFallback: boolean, + ): ResolvedTaxCode { + return { + id: tc.id, + code: tc.code, + name: tc.name, + rate: Number(tc.rate), + isTaxInclusive: tc.isTaxInclusive, + taxNature: tc.taxNature, + outputAccountId: tc.outputAccountId, + inputAccountId: tc.inputAccountId, + accountId: tc.accountId, + isFallback, + }; + } +} diff --git a/apps/api/src/core/workflow/dto/transition-workflow.dto.ts b/apps/api/src/core/workflow/dto/transition-workflow.dto.ts index fdfc322..4bd8c42 100644 --- a/apps/api/src/core/workflow/dto/transition-workflow.dto.ts +++ b/apps/api/src/core/workflow/dto/transition-workflow.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsObject } from 'class-validator'; import { IsOptional, IsString } from 'class-validator'; +import { Prisma } from '@prisma/client'; export class TransitionWorkflowDto { @ApiProperty({ description: '动作编码,如 ship/cancel/start' }) @@ -20,5 +21,5 @@ export class TransitionWorkflowDto { @IsOptional() @IsObject() @Type(() => Object) - data?: Record; + data?: Prisma.InputJsonObject; } diff --git a/apps/api/src/core/workflow/workflow.service.ts b/apps/api/src/core/workflow/workflow.service.ts index 36b7411..2cf4d30 100644 --- a/apps/api/src/core/workflow/workflow.service.ts +++ b/apps/api/src/core/workflow/workflow.service.ts @@ -5,6 +5,7 @@ import { NotFoundException, } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; interface WorkflowTargetConfig { @@ -100,7 +101,7 @@ interface WorkflowTransitionTransaction { action: string; entity: string; entityId: string; - details: Record; + details: Prisma.InputJsonValue; companyId: string; }; }): Promise; @@ -201,7 +202,7 @@ export class WorkflowService { companyId: string, operatorId: string, note?: string, - extraData?: Record, + extraData?: Prisma.InputJsonObject, ): Promise { const normalizedModel = this.normalizeModelName(modelName); const target = WORKFLOW_TARGETS[normalizedModel]; @@ -224,11 +225,12 @@ export class WorkflowService { whereCondition[target.companyField] = companyId; } - // @ts-expect-error TODO(strict): Prisma $transaction overload inference issue const { updatedRecord, matchedTransition }: WorkflowTransitionTxResult = - // @ts-expect-error TODO(strict): Prisma transaction client typing needs full Prisma.TransactionClient - await this.prisma.$transaction(async (tx: WorkflowTransitionTransaction): Promise => { - const txDelegate = tx[target.delegate as WorkflowDelegateName]; + await this.prisma.$transaction( + async (tx): Promise => { + const workflowTx = tx as unknown as WorkflowTransitionTransaction; + const txDelegate = + workflowTx[target.delegate as WorkflowDelegateName]; if (!txDelegate) { throw new BadRequestException(`模型 ${modelName} delegate 不存在`); @@ -239,7 +241,11 @@ export class WorkflowService { throw new NotFoundException('目标业务单据不存在或无权限访问'); } - const currentState = String(record[target.statusField] ?? ''); + const rawState = record[target.statusField]; + const currentState = + typeof rawState === 'string' || typeof rawState === 'number' + ? String(rawState) + : ''; const matched = workflow.transitions.find( (item) => item.action === action && item.fromState.value === currentState, @@ -269,7 +275,7 @@ export class WorkflowService { where: { id: recordId }, }); - await tx.auditLog.create({ + await workflowTx.auditLog.create({ data: { userId: operatorId, action: 'WORKFLOW_TRANSITION', diff --git a/apps/api/src/finance/accounting.service.spec.ts b/apps/api/src/finance/accounting.service.spec.ts new file mode 100644 index 0000000..46297db --- /dev/null +++ b/apps/api/src/finance/accounting.service.spec.ts @@ -0,0 +1,489 @@ +import { BadRequestException } from '@nestjs/common'; +import { JournalType, TaxNature } from '@prisma/client'; +import { AccountingService } from './accounting.service'; + +const mockTaxService = { + round2: jest.fn((v: number) => Math.round((v + Number.EPSILON) * 100) / 100), +}; + +const mockTx = { + $executeRawUnsafe: jest.fn(), + journal: { upsert: jest.fn() }, + journalEntry: { create: jest.fn() }, + account: { upsert: jest.fn() }, +}; + +const mockPrisma = { + journal: { upsert: jest.fn() }, + journalEntry: { findFirst: jest.fn(), create: jest.fn() }, + material: { findFirst: jest.fn() }, + account: { findFirst: jest.fn(), upsert: jest.fn() }, + purchaseInvoice: { findFirst: jest.fn() }, + invoice: { findFirst: jest.fn() }, + $transaction: jest.fn(), + $executeRawUnsafe: jest.fn(), +}; + +type CreatedJournalLine = { + lineNo: number; + accountId: string; + partnerId?: string; + debit: number; + credit: number; + memo?: string; +}; + +type JournalEntryCreateCall = { + data: { + lines: { + create: CreatedJournalLine[]; + }; + }; +}; + +function getLastCreatedJournalLines(): CreatedJournalLine[] { + const call = mockTx.journalEntry.create.mock.calls.at(-1)?.[0] as + | JournalEntryCreateCall + | undefined; + return call?.data.lines.create ?? []; +} + +describe('AccountingService', () => { + let service: AccountingService; + + beforeEach(() => { + jest.clearAllMocks(); + mockPrisma.$transaction.mockImplementation( + (cb: (tx: typeof mockTx) => unknown) => cb(mockTx), + ); + mockTx.journal.upsert.mockResolvedValue({ id: 'journal1', code: 'GEN' }); + mockTx.journalEntry.create.mockResolvedValue({ + id: 'entry1', + entryNo: 'JE-001', + lines: [], + journal: { code: 'GEN' }, + }); + mockTx.account.upsert.mockResolvedValue({ id: 'acc1' }); + service = new AccountingService( + mockPrisma as unknown as ConstructorParameters< + typeof AccountingService + >[0], + mockTaxService as unknown as ConstructorParameters< + typeof AccountingService + >[1], + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('createBalancedEntry', () => { + it('should create a balanced entry', async () => { + await service.createBalancedEntry({ + companyId: 'c1', + journalCode: 'GEN', + journalName: 'General', + journalType: JournalType.GENERAL, + ref: 'T1', + lines: [ + { + accountCode: '1122', + accountName: 'AR', + accountType: 'ASSET', + debit: 1000, + }, + { + accountCode: '6001', + accountName: 'Rev', + accountType: 'REVENUE', + credit: 885, + }, + { + accountCode: '222101', + accountName: 'Tax', + accountType: 'LIABILITY', + credit: 115, + }, + ], + }); + expect(mockTx.journalEntry.create).toHaveBeenCalled(); + }); + + it('should throw when debit != credit', async () => { + await expect( + service.createBalancedEntry({ + companyId: 'c1', + journalCode: 'GEN', + journalName: 'General', + journalType: JournalType.GENERAL, + lines: [ + { + accountCode: '1122', + accountName: 'AR', + accountType: 'ASSET', + debit: 1000, + }, + { + accountCode: '6001', + accountName: 'Rev', + accountType: 'REVENUE', + credit: 500, + }, + ], + }), + ).rejects.toThrow(BadRequestException); + }); + + it('should throw when lines are empty', async () => { + await expect( + service.createBalancedEntry({ + companyId: 'c1', + journalCode: 'GEN', + journalName: 'General', + journalType: JournalType.GENERAL, + lines: [], + }), + ).rejects.toThrow(); + }); + + it('should throw when a line has both debit and credit', async () => { + await expect( + service.createBalancedEntry({ + companyId: 'c1', + journalCode: 'GEN', + journalName: 'General', + journalType: JournalType.GENERAL, + lines: [ + { + accountCode: '1122', + accountName: 'AR', + accountType: 'ASSET', + debit: 500, + credit: 500, + }, + ], + }), + ).rejects.toThrow(); + }); + + it('should throw when amount is negative', async () => { + await expect( + service.createBalancedEntry({ + companyId: 'c1', + journalCode: 'GEN', + journalName: 'General', + journalType: JournalType.GENERAL, + lines: [ + { + accountCode: '1122', + accountName: 'AR', + accountType: 'ASSET', + debit: -100, + }, + { + accountCode: '6001', + accountName: 'Rev', + accountType: 'REVENUE', + credit: 100, + }, + ], + }), + ).rejects.toThrow(); + }); + }); + + describe('postStockDepletedEntry', () => { + it('should create COGS entry: Debit 6401, Credit 1405', async () => { + mockPrisma.material.findFirst.mockResolvedValue({ + name: 'Steel', + unitPrice: 50, + }); + const result = await service.postStockDepletedEntry({ + companyId: 'c1', + materialId: 'mat1', + quantity: 10, + unitCost: 50, + operatorId: 'u1', + }); + expect(result).toBeDefined(); + expect(mockTx.journalEntry.create).toHaveBeenCalled(); + }); + + it('should skip when amount is 0', async () => { + mockPrisma.material.findFirst.mockResolvedValue({ + name: 'Free', + unitPrice: 0, + }); + const result = await service.postStockDepletedEntry({ + companyId: 'c1', + materialId: 'mat1', + quantity: 10, + unitCost: 0, + }); + expect(result).toBeNull(); + }); + }); + + describe('postInvoicePostedEntry', () => { + it('should create sales entry: Debit AR, Credit Revenue + Output Tax', async () => { + mockPrisma.invoice.findFirst.mockResolvedValue({ + id: 'inv1', + invoiceNo: 'INV-001', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxNature: TaxNature.OUTPUT, + taxCode: null, + order: { orderNo: 'ORD-001', partnerId: 'p1' }, + }); + const result = await service.postInvoicePostedEntry({ + companyId: 'c1', + invoiceId: 'inv1', + operatorId: 'u1', + }); + expect(result).toBeDefined(); + expect(mockTx.journal.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { companyId_code: { companyId: 'c1', code: 'SAL' } }, + }), + ); + }); + + it('should throw when invoice not found', async () => { + mockPrisma.invoice.findFirst.mockResolvedValue(null); + await expect( + service.postInvoicePostedEntry({ + companyId: 'c1', + invoiceId: 'x', + operatorId: 'u1', + }), + ).rejects.toThrow(); + }); + + it('should use fallback tax when snapshot missing', async () => { + mockPrisma.invoice.findFirst.mockResolvedValue({ + id: 'inv2', + invoiceNo: 'INV-002', + amount: 1130, + subTotal: 0, + taxAmount: 0, + taxRate: 0.13, + taxNature: TaxNature.OUTPUT, + taxCode: null, + order: { orderNo: 'ORD-002', partnerId: 'p1' }, + }); + const result = await service.postInvoicePostedEntry({ + companyId: 'c1', + invoiceId: 'inv2', + operatorId: 'u1', + }); + expect(result).toBeDefined(); + }); + }); + + describe('postVendorBillPostedEntry', () => { + it('should use default 1401 when lines have no accountId', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue({ + id: 'pi1', + invoiceNo: 'VB-001', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxNature: TaxNature.INPUT, + taxCode: null, + partnerId: 'p1', + partner: { id: 'p1', name: 'Supplier A' }, + lines: [ + { subTotal: 600, accountId: null, account: null }, + { subTotal: 400, accountId: null, account: null }, + ], + }); + const result = await service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'pi1', + operatorId: 'u1', + }); + expect(result).toBeDefined(); + expect(mockTx.account.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { companyId_code: { companyId: 'c1', code: '1401' } }, + create: expect.objectContaining({ name: 'Inventory/Raw Materials' }), + }), + ); + expect(getLastCreatedJournalLines()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ debit: 1000, credit: 0 }), + expect.objectContaining({ debit: 130, credit: 0 }), + expect.objectContaining({ debit: 0, credit: 1130 }), + ]), + ); + }); + + it('should support mixed inventory + expense accounts', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue({ + id: 'pi2', + invoiceNo: 'VB-002', + amount: 2260, + subTotal: 2000, + taxAmount: 260, + taxRate: 0.13, + taxNature: TaxNature.INPUT, + taxCode: null, + partnerId: 'p1', + partner: { id: 'p1', name: 'Supplier B' }, + lines: [ + { + subTotal: 1000, + accountId: 'acc-inv', + account: { code: '1401', name: 'Inventory', type: 'ASSET' }, + }, + { + subTotal: 500, + accountId: 'acc-exp', + account: { code: '6601', name: 'Admin Expense', type: 'EXPENSE' }, + }, + { + subTotal: 500, + accountId: 'acc-exp', + account: { code: '6601', name: 'Admin Expense', type: 'EXPENSE' }, + }, + ], + }); + const result = await service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'pi2', + operatorId: 'u1', + }); + expect(result).toBeDefined(); + expect(mockTx.account.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { companyId_code: { companyId: 'c1', code: '6601' } }, + create: expect.objectContaining({ name: 'Admin Expense' }), + }), + ); + expect(getLastCreatedJournalLines()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ debit: 1000, credit: 0 }), + expect.objectContaining({ debit: 1000, credit: 0 }), + expect.objectContaining({ debit: 260, credit: 0 }), + expect.objectContaining({ debit: 0, credit: 2260 }), + ]), + ); + }); + + it('should fallback to invoice-level subTotal when lines empty', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue({ + id: 'pi3', + invoiceNo: 'VB-003', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxNature: TaxNature.INPUT, + taxCode: null, + partnerId: 'p1', + partner: { id: 'p1', name: 'Supplier C' }, + lines: [], + }); + const result = await service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'pi3', + operatorId: 'u1', + }); + expect(result).toBeDefined(); + }); + + it('should throw when purchase invoice not found', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue(null); + await expect( + service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'x', + operatorId: 'u1', + }), + ).rejects.toThrow(); + }); + + it('should throw when amount <= 0', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue({ + id: 'pi4', + invoiceNo: 'VB-004', + amount: 0, + subTotal: 0, + taxAmount: 0, + partnerId: 'p1', + partner: { id: 'p1', name: 'Supplier D' }, + lines: [], + }); + await expect( + service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'pi4', + operatorId: 'u1', + }), + ).rejects.toThrow(); + }); + }); + + describe('Balance verification', () => { + it('sales: AR(1130) = Revenue(1000) + Tax(130)', async () => { + mockPrisma.invoice.findFirst.mockResolvedValue({ + id: 'inv10', + invoiceNo: 'INV-010', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxNature: TaxNature.OUTPUT, + taxCode: null, + order: { orderNo: 'ORD-010', partnerId: 'p1' }, + }); + await service.postInvoicePostedEntry({ + companyId: 'c1', + invoiceId: 'inv10', + operatorId: 'u1', + }); + expect(mockTx.journalEntry.create).toHaveBeenCalled(); + }); + + it('purchase: Inventory(1000) + Tax(130) = AP(1130)', async () => { + mockPrisma.purchaseInvoice.findFirst.mockResolvedValue({ + id: 'pi10', + invoiceNo: 'VB-010', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxNature: TaxNature.INPUT, + taxCode: null, + partnerId: 'p1', + partner: { id: 'p1', name: 'Supplier X' }, + lines: [{ subTotal: 1000, accountId: null, account: null }], + }); + await service.postVendorBillPostedEntry({ + companyId: 'c1', + invoiceId: 'pi10', + operatorId: 'u1', + }); + expect(mockTx.journalEntry.create).toHaveBeenCalled(); + }); + + it('stock depletion: COGS(500) = Inventory(500)', async () => { + mockPrisma.material.findFirst.mockResolvedValue({ + name: 'Widget', + unitPrice: 50, + }); + await service.postStockDepletedEntry({ + companyId: 'c1', + materialId: 'mat10', + quantity: 10, + unitCost: 50, + operatorId: 'u1', + }); + expect(mockTx.journalEntry.create).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/api/src/finance/accounting.service.ts b/apps/api/src/finance/accounting.service.ts index a34214e..c66a4f5 100644 --- a/apps/api/src/finance/accounting.service.ts +++ b/apps/api/src/finance/accounting.service.ts @@ -1,6 +1,12 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { EntryPostingStatus, JournalType, Prisma } from '@prisma/client'; +import { + EntryPostingStatus, + JournalType, + TaxNature, + Prisma, +} from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { TaxService } from '../core/tax/tax.service'; interface JournalLineInput { accountCode: string; @@ -28,7 +34,10 @@ interface CreateBalancedEntryInput { export class AccountingService { private readonly logger = new Logger(AccountingService.name); - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly taxService: TaxService, + ) {} async postStockDepletedEntry(payload: { companyId: string; @@ -44,11 +53,11 @@ export class AccountingService { }); const unitCost = Number(payload.unitCost ?? material?.unitPrice ?? 0); - const amount = this.round2(unitCost * Number(payload.quantity ?? 0)); + const amount = this.taxService.round2( + unitCost * Number(payload.quantity ?? 0), + ); if (amount <= 0) { - this.logger.warn( - `跳过零成本库存出库凭证: material=${payload.materialId}`, - ); + this.logger.warn('跳过零成本库存出库凭�? material=' + payload.materialId); return null; } @@ -58,7 +67,8 @@ export class AccountingService { journalName: 'Inventory Journal', journalType: JournalType.INVENTORY, ref: payload.referenceNo, - description: `库存出库自动凭证: ${material?.name ?? payload.materialId}`, + description: + '库存出库自动凭证: ' + (material?.name ?? payload.materialId), createdBy: payload.operatorId, lines: [ { @@ -83,6 +93,7 @@ export class AccountingService { companyId: string; invoiceId: string; taxCodeId?: string | null; + taxAccountId?: string | null; taxRate?: number; operatorId?: string; }) { @@ -90,7 +101,13 @@ export class AccountingService { where: { id: payload.invoiceId, companyId: payload.companyId }, include: { order: { select: { orderNo: true, partnerId: true } }, - taxCode: { include: { account: true } }, + taxCode: { + include: { + account: true, + outputAccount: true, + inputAccount: true, + }, + }, }, }); @@ -98,47 +115,69 @@ export class AccountingService { throw new BadRequestException('发票不存在,无法生成凭证'); } - const amount = this.round2(Number(invoice.amount)); + const amount = this.taxService.round2(Number(invoice.amount)); if (amount <= 0) { throw new BadRequestException('发票金额必须大于0'); } - let revenue = this.round2(Number(invoice.subTotal ?? 0)); - let tax = this.round2(Number(invoice.taxAmount ?? 0)); + // 使用发票快照中的价税数据,不重新计算 + let revenue = this.taxService.round2(Number(invoice.subTotal ?? 0)); + let tax = this.taxService.round2(Number(invoice.taxAmount ?? 0)); if (revenue <= 0 && tax <= 0) { + // 仅对历史遗留数据做兜�? const fallbackRate = Math.max( 0, Math.min( 1, - Number(payload.taxRate ?? invoice.taxCode?.rate ?? 0.13), + Number( + invoice.taxRate ?? payload.taxRate ?? invoice.taxCode?.rate ?? 0.13, + ), ), ); - revenue = this.round2(amount / (1 + fallbackRate)); - tax = this.round2(amount - revenue); + revenue = this.taxService.round2(amount / (1 + fallbackRate)); + tax = this.taxService.round2(amount - revenue); this.logger.warn( - `发票未包含税额快照,使用兜底税率计算: invoice=${invoice.invoiceNo}`, + '发票未包含税额快照,使用兜底税率计算: invoice=' + invoice.invoiceNo, ); } - const resolvedTaxCode = invoice.taxCode - ? invoice.taxCode - : payload.taxCodeId - ? await this.prisma.taxCode.findFirst({ - where: { - id: payload.taxCodeId, - companyId: payload.companyId, - active: true, - }, - include: { account: true }, - }) - : null; + // 确定税务科目 + let taxAccountCode = '222101'; + let taxAccountName = '应交税费-销项税'; + let taxAccountType = 'LIABILITY'; - const taxAccount = resolvedTaxCode?.account; - if (!taxAccount) { - this.logger.warn( - `未配置税码会计科目,使用默认销项税科目: invoice=${invoice.invoiceNo}`, - ); + if (payload.taxAccountId) { + const account = await this.prisma.account.findFirst({ + where: { id: payload.taxAccountId, companyId: payload.companyId }, + }); + if (account) { + taxAccountCode = account.code; + taxAccountName = account.name; + taxAccountType = account.type; + } + } else if (invoice.taxCode) { + const tc = invoice.taxCode; + const nature = tc.taxNature ?? TaxNature.OUTPUT; + const accountId = + nature === TaxNature.INPUT + ? (tc.inputAccountId ?? tc.accountId) + : (tc.outputAccountId ?? tc.accountId); + if (accountId) { + const account = await this.prisma.account.findFirst({ + where: { id: accountId, companyId: payload.companyId }, + }); + if (account) { + taxAccountCode = account.code; + taxAccountName = account.name; + taxAccountType = account.type; + } + } else { + this.logger.warn( + '未配置税码会计科目,使用默认销项税科目: invoice=' + + invoice.invoiceNo, + ); + } } return this.createBalancedEntry({ @@ -147,7 +186,7 @@ export class AccountingService { journalName: 'Sales Journal', journalType: JournalType.SALES, ref: invoice.invoiceNo, - description: `销售开票自动凭证: ${invoice.invoiceNo}`, + description: '销售开票自动凭�? ' + invoice.invoiceNo, createdBy: payload.operatorId, lines: [ { @@ -156,7 +195,7 @@ export class AccountingService { accountType: 'ASSET', debit: amount, partnerId: invoice.order.partnerId, - memo: `应收 ${invoice.invoiceNo}`, + memo: '应收 ' + invoice.invoiceNo, }, { accountCode: '6001', @@ -164,45 +203,245 @@ export class AccountingService { accountType: 'REVENUE', credit: revenue, partnerId: invoice.order.partnerId, - memo: `收入 ${invoice.invoiceNo}`, + memo: '收入 ' + invoice.invoiceNo, }, { - accountCode: taxAccount?.code ?? '222101', - accountName: taxAccount?.name ?? '应交税费-销项税', - accountType: taxAccount?.type ?? 'LIABILITY', + accountCode: taxAccountCode, + accountName: taxAccountName, + accountType: taxAccountType, credit: tax, - memo: `销项税 ${invoice.invoiceNo}`, + memo: '销项税 ' + invoice.invoiceNo, }, ], }); } + async postVendorBillPostedEntry(payload: { + companyId: string; + invoiceId: string; + taxCodeId?: string | null; + taxAccountId?: string | null; + taxRate?: number; + operatorId?: string; + }) { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id: payload.invoiceId, companyId: payload.companyId }, + include: { + partner: { select: { id: true, name: true } }, + taxCode: { + include: { account: true, outputAccount: true, inputAccount: true }, + }, + lines: { include: { account: true } }, + }, + }); + if (!invoice) throw new BadRequestException('Purchase invoice not found'); + + const amount = this.taxService.round2(Number(invoice.amount)); + if (amount <= 0) + throw new BadRequestException('Purchase invoice amount must be > 0'); + + let subTotal = this.taxService.round2(Number(invoice.subTotal ?? 0)); + let taxAmount = this.taxService.round2(Number(invoice.taxAmount ?? 0)); + if (subTotal <= 0 && taxAmount <= 0) { + const fallbackRate = Math.max( + 0, + Math.min( + 1, + Number( + invoice.taxRate ?? payload.taxRate ?? invoice.taxCode?.rate ?? 0.13, + ), + ), + ); + subTotal = this.taxService.round2(amount / (1 + fallbackRate)); + taxAmount = this.taxService.round2(amount - subTotal); + this.logger.warn( + 'Purchase invoice missing tax snapshot, using fallback rate: ' + + invoice.invoiceNo, + ); + } + + // ---- Resolve input tax account ---- + let taxAccountCode = '222102'; + let taxAccountName = 'Tax Payable - Input Tax'; + let taxAccountType = 'LIABILITY'; + if (payload.taxAccountId) { + const account = await this.prisma.account.findFirst({ + where: { id: payload.taxAccountId, companyId: payload.companyId }, + }); + if (account) { + taxAccountCode = account.code; + taxAccountName = account.name; + taxAccountType = account.type; + } + } else if (invoice.taxCode) { + const tc = invoice.taxCode; + const nature = tc.taxNature ?? TaxNature.INPUT; + const accountId = + nature === TaxNature.INPUT + ? (tc.inputAccountId ?? tc.accountId) + : (tc.outputAccountId ?? tc.accountId); + if (accountId) { + const account = await this.prisma.account.findFirst({ + where: { id: accountId, companyId: payload.companyId }, + }); + if (account) { + taxAccountCode = account.code; + taxAccountName = account.name; + taxAccountType = account.type; + } + } + } + + // ---- Build debit lines by line-level account (inventory / expense) ---- + const debitLines = this.buildPurchaseDebitLines( + invoice.lines.map((l) => ({ + ...l, + subTotal: Number(l.subTotal), + })), + invoice.invoiceNo, + invoice.partnerId, + subTotal, + ); + + return this.createBalancedEntry({ + companyId: payload.companyId, + journalCode: 'PUR', + journalName: 'Purchase Journal', + journalType: JournalType.PURCHASE, + ref: invoice.invoiceNo, + description: 'Purchase AP auto-entry: ' + invoice.invoiceNo, + createdBy: payload.operatorId, + lines: [ + ...debitLines, + { + accountCode: taxAccountCode, + accountName: taxAccountName, + accountType: taxAccountType, + debit: taxAmount, + memo: 'Input Tax ' + invoice.invoiceNo, + }, + { + accountCode: '2202', + accountName: 'Accounts Payable', + accountType: 'LIABILITY', + credit: amount, + partnerId: invoice.partnerId, + memo: 'AP ' + invoice.invoiceNo, + }, + ], + }); + } + + /** + * Build debit journal lines from purchase invoice lines. + * Uses line-level accountId (inventory or expense); defaults to 1401. + * Merges lines sharing the same account code into a single debit line. + */ + private buildPurchaseDebitLines( + lines: Array<{ + subTotal: number; + accountId?: string | null; + account?: { code: string; name: string; type: string } | null; + }>, + invoiceNo: string, + partnerId: string, + fallbackSubTotal: number, + ): JournalLineInput[] { + const accountMap = new Map< + string, + { code: string; name: string; type: string; amount: number } + >(); + + if (lines.length > 0) { + for (const line of lines) { + const lineSubTotal = this.taxService.round2(Number(line.subTotal ?? 0)); + if (lineSubTotal <= 0) continue; + + if (line.account) { + const key = line.account.code; + const existing = accountMap.get(key); + if (existing) { + existing.amount = this.taxService.round2( + existing.amount + lineSubTotal, + ); + } else { + accountMap.set(key, { + code: line.account.code, + name: line.account.name, + type: line.account.type, + amount: lineSubTotal, + }); + } + } else { + const key = '1401'; + const existing = accountMap.get(key); + if (existing) { + existing.amount = this.taxService.round2( + existing.amount + lineSubTotal, + ); + } else { + accountMap.set(key, { + code: '1401', + name: 'Inventory/Raw Materials', + type: 'ASSET', + amount: lineSubTotal, + }); + } + } + } + } + + // Fallback: if no line-level data, use invoice-level subTotal + if (accountMap.size === 0) { + accountMap.set('1401', { + code: '1401', + name: 'Inventory/Raw Materials', + type: 'ASSET', + amount: fallbackSubTotal, + }); + } + + const result: JournalLineInput[] = []; + for (const entry of accountMap.values()) { + if (entry.amount > 0) { + result.push({ + accountCode: entry.code, + accountName: entry.name, + accountType: entry.type, + debit: entry.amount, + partnerId, + memo: 'Purchase ' + invoiceNo, + }); + } + } + return result; + } async createBalancedEntry(input: CreateBalancedEntryInput) { await this.ensureDefaultMasterData(input.companyId); return this.prisma.$transaction(async (tx) => { await tx.$executeRawUnsafe( 'SELECT pg_advisory_xact_lock(hashtext($1))', - `journal-entry-${input.companyId}`, + 'journal-entry-' + input.companyId, ); const lines = input.lines.map((line) => ({ ...line, - debit: this.round2(Number(line.debit ?? 0)), - credit: this.round2(Number(line.credit ?? 0)), + debit: this.taxService.round2(Number(line.debit ?? 0)), + credit: this.taxService.round2(Number(line.credit ?? 0)), })); this.validateLines(lines); - const totalDebit = this.round2( + const totalDebit = this.taxService.round2( lines.reduce((sum, line) => sum + line.debit, 0), ); - const totalCredit = this.round2( + const totalCredit = this.taxService.round2( lines.reduce((sum, line) => sum + line.credit, 0), ); if (totalDebit !== totalCredit) { throw new BadRequestException( - `借贷不平衡: debit=${totalDebit}, credit=${totalCredit}`, + '借贷不平�? debit=' + totalDebit + ', credit=' + totalCredit, ); } @@ -344,14 +583,250 @@ export class AccountingService { private generateEntryNo() { const now = new Date(); - const datePart = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String( - now.getDate(), - ).padStart(2, '0')}`; + const datePart = + now.getFullYear() + + String(now.getMonth() + 1).padStart(2, '0') + + String(now.getDate()).padStart(2, '0'); const suffix = String(now.getTime()).slice(-6); - return `JE-${datePart}-${suffix}`; + return 'JE-' + datePart + '-' + suffix; + } + + // ─── 凭证冲销 ────────────────────────────────────── + + /** + * 冲销已过账凭证 —— 生成反向借贷分录,原凭证标记为 REVERSED。 + * 禁止删除历史凭证,确保完整审计轨迹。 + * 幂等:如果原凭证已被冲销,直接返回已有冲销凭证。 + */ + async reverseJournalEntry( + companyId: string, + entryId: string, + operatorId: string, + reason?: string, + ) { + const original = await this.prisma.journalEntry.findFirst({ + where: { id: entryId, companyId }, + include: { + lines: { include: { account: true }, orderBy: { lineNo: 'asc' } }, + journal: true, + }, + }); + + if (!original) { + throw new BadRequestException('凭证不存在'); + } + + if (original.postingStatus === EntryPostingStatus.REVERSED) { + // 幂等: 查找已存在的冲销凭证 + const existingReversal = await this.prisma.journalEntry.findFirst({ + where: { companyId, reversedEntryId: original.id }, + include: { lines: { include: { account: true } } }, + }); + return { + original: { id: original.id, entryNo: original.entryNo }, + reversal: existingReversal, + message: '凭证已冲销,返回已有冲销凭证', + }; + } + + if (original.postingStatus !== EntryPostingStatus.POSTED) { + throw new BadRequestException( + '只有已过账状态的凭证才能冲销,当前状态: ' + original.postingStatus, + ); + } + + if (original.lines.length === 0) { + throw new BadRequestException('凭证无分录,无法冲销'); + } + + // 生成反向分录:借方 ↔ 贷方 + const reversalDescription = + `冲销凭证 ${original.entryNo}` + (reason ? ` (原因: ${reason})` : ''); + + return this.prisma.$transaction(async (tx) => { + await tx.$executeRawUnsafe( + 'SELECT pg_advisory_xact_lock(hashtext($1))', + 'journal-entry-' + companyId, + ); + + // 1. 标记原凭证为 REVERSED + await tx.journalEntry.update({ + where: { id: original.id }, + data: { postingStatus: EntryPostingStatus.REVERSED }, + }); + + // 2. 生成冲销凭证 + const reversalEntry = await tx.journalEntry.create({ + data: { + entryNo: this.generateEntryNo(), + date: new Date(), + ref: original.ref ? `REV-${original.ref}` : `REV-${original.entryNo}`, + description: reversalDescription, + journalId: original.journalId, + companyId, + createdBy: operatorId, + postingStatus: EntryPostingStatus.POSTED, + reversedEntryId: original.id, + postedAt: new Date(), + lines: { + create: original.lines.map((line, index) => ({ + lineNo: index + 1, + accountId: line.accountId, + partnerId: line.partnerId, + debit: line.credit, // 借贷互换 + credit: line.debit, // 借贷互换 + memo: `冲销: ${line.memo ?? ''}`, + })), + }, + }, + include: { + lines: { include: { account: true }, orderBy: { lineNo: 'asc' } }, + journal: true, + }, + }); + + // 3. 审计日志 + await tx.auditLog.create({ + data: { + userId: operatorId, + action: 'REVERSE_JOURNAL_ENTRY', + entity: 'JournalEntry', + entityId: original.id, + details: { + originalEntryNo: original.entryNo, + reversalEntryNo: reversalEntry.entryNo, + reason: reason ?? null, + totalDebit: this.taxService.round2( + original.lines.reduce((s, l) => s + Number(l.debit), 0), + ), + totalCredit: this.taxService.round2( + original.lines.reduce((s, l) => s + Number(l.credit), 0), + ), + }, + companyId, + }, + }); + + return { + original: { + id: original.id, + entryNo: original.entryNo, + postingStatus: EntryPostingStatus.REVERSED, + }, + reversal: reversalEntry, + message: '凭证冲销成功', + }; + }); } - private round2(value: number) { - return Number((value + Number.EPSILON).toFixed(2)); + // ─── 试算平衡表 ───────────────────────────────────── + + /** + * 试算平衡表 —— 按期间、科目、公司聚合所有已过账凭证的借贷合计。 + * 只统计 POSTED 状态的凭证。 + */ + async getTrialBalance( + companyId: string, + startDate?: Date, + endDate?: Date, + accountType?: string, + ) { + const dateFilter: { gte?: Date; lte?: Date } = {}; + if (startDate) dateFilter.gte = startDate; + if (endDate) dateFilter.lte = endDate; + + const where: { + journalEntry: { + companyId: string; + postingStatus: typeof EntryPostingStatus.POSTED; + date?: { gte?: Date; lte?: Date }; + }; + account?: { type: string }; + } = { + journalEntry: { + companyId, + postingStatus: EntryPostingStatus.POSTED, + }, + }; + + if (startDate || endDate) { + where.journalEntry.date = dateFilter; + } + if (accountType) { + where.account = { type: accountType }; + } + + const lines = await this.prisma.journalEntryLine.findMany({ + where, + include: { + account: { select: { id: true, code: true, name: true, type: true } }, + }, + }); + + // 按科目聚合 + const accountMap = new Map< + string, + { + accountId: string; + accountCode: string; + accountName: string; + accountType: string; + totalDebit: number; + totalCredit: number; + } + >(); + + for (const line of lines) { + const key = line.accountId; + const existing = accountMap.get(key); + if (existing) { + existing.totalDebit = this.taxService.round2( + existing.totalDebit + Number(line.debit), + ); + existing.totalCredit = this.taxService.round2( + existing.totalCredit + Number(line.credit), + ); + } else { + accountMap.set(key, { + accountId: line.account.id, + accountCode: line.account.code, + accountName: line.account.name, + accountType: line.account.type, + totalDebit: this.taxService.round2(Number(line.debit)), + totalCredit: this.taxService.round2(Number(line.credit)), + }); + } + } + + const rows = Array.from(accountMap.values()) + .map((row) => ({ + ...row, + balance: this.taxService.round2(row.totalDebit - row.totalCredit), + })) + .sort((a, b) => a.accountCode.localeCompare(b.accountCode)); + + const grandTotalDebit = this.taxService.round2( + rows.reduce((s, r) => s + r.totalDebit, 0), + ); + const grandTotalCredit = this.taxService.round2( + rows.reduce((s, r) => s + r.totalCredit, 0), + ); + + return { + companyId, + period: { + startDate: startDate?.toISOString() ?? null, + endDate: endDate?.toISOString() ?? null, + }, + rows, + summary: { + totalDebit: grandTotalDebit, + totalCredit: grandTotalCredit, + difference: this.taxService.round2(grandTotalDebit - grandTotalCredit), + isBalanced: grandTotalDebit === grandTotalCredit, + accountCount: rows.length, + }, + generatedAt: new Date(), + }; } } diff --git a/apps/api/src/finance/dto/finance.dto.ts b/apps/api/src/finance/dto/finance.dto.ts index 152bdf3..464b45d 100644 --- a/apps/api/src/finance/dto/finance.dto.ts +++ b/apps/api/src/finance/dto/finance.dto.ts @@ -19,7 +19,10 @@ export class CreateInvoiceDto { @Min(0) amount!: number; - @ApiProperty({ description: '税码ID (建议填写,未填将使用默认税码)', required: false }) + @ApiProperty({ + description: '税码ID (建议填写,未填将使用默认税码)', + required: false, + }) @IsOptional() @IsString() taxCodeId?: string; diff --git a/apps/api/src/finance/dto/tax-code.dto.ts b/apps/api/src/finance/dto/tax-code.dto.ts new file mode 100644 index 0000000..db0b3b2 --- /dev/null +++ b/apps/api/src/finance/dto/tax-code.dto.ts @@ -0,0 +1,113 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsString, + IsNotEmpty, + IsNumber, + IsOptional, + IsBoolean, + IsEnum, + Min, + Max, +} from 'class-validator'; +import { TaxNature } from '@prisma/client'; + +export class CreateTaxCodeDto { + @ApiProperty({ description: '税码编码,如 VAT_13、GST_5' }) + @IsString() + @IsNotEmpty() + code!: string; + + @ApiProperty({ description: '税码名称' }) + @IsString() + @IsNotEmpty() + name!: string; + + @ApiProperty({ description: '税率 (0-1)', example: 0.13 }) + @IsNumber() + @Min(0) + @Max(1) + rate!: number; + + @ApiProperty({ description: '是否含税计价', default: true }) + @IsOptional() + @IsBoolean() + isTaxInclusive?: boolean; + + @ApiProperty({ description: '设置为公司默认税码', default: false }) + @IsOptional() + @IsBoolean() + isDefault?: boolean; + + @ApiPropertyOptional({ + description: '税务属性: OUTPUT=销项税, INPUT=进项税', + enum: TaxNature, + default: TaxNature.OUTPUT, + }) + @IsOptional() + @IsEnum(TaxNature) + taxNature?: TaxNature; + + @ApiPropertyOptional({ description: '(旧)税务科目 ID' }) + @IsOptional() + @IsString() + accountId?: string; + + @ApiPropertyOptional({ description: '销项税科目 ID' }) + @IsOptional() + @IsString() + outputAccountId?: string; + + @ApiPropertyOptional({ description: '进项税科目 ID' }) + @IsOptional() + @IsString() + inputAccountId?: string; +} + +export class UpdateTaxCodeDto { + @ApiPropertyOptional({ description: '税码名称' }) + @IsOptional() + @IsString() + name?: string; + + @ApiPropertyOptional({ description: '税率 (0-1)', example: 0.13 }) + @IsOptional() + @IsNumber() + @Min(0) + @Max(1) + rate?: number; + + @ApiPropertyOptional({ description: '是否含税计价' }) + @IsOptional() + @IsBoolean() + isTaxInclusive?: boolean; + + @ApiPropertyOptional({ description: '是否默认税码' }) + @IsOptional() + @IsBoolean() + isDefault?: boolean; + + @ApiPropertyOptional({ description: '是否启用' }) + @IsOptional() + @IsBoolean() + active?: boolean; + + @ApiPropertyOptional({ description: '税务属性', enum: TaxNature }) + @IsOptional() + @IsEnum(TaxNature) + taxNature?: TaxNature; + + @ApiPropertyOptional({ description: '销项税科目 ID' }) + @IsOptional() + @IsString() + outputAccountId?: string; + + @ApiPropertyOptional({ description: '进项税科目 ID' }) + @IsOptional() + @IsString() + inputAccountId?: string; + + @ApiPropertyOptional({ description: '(旧)税务科目 ID' }) + @IsOptional() + @IsString() + accountId?: string; +} diff --git a/apps/api/src/finance/dto/vendor-bill.dto.ts b/apps/api/src/finance/dto/vendor-bill.dto.ts new file mode 100644 index 0000000..67bcaea --- /dev/null +++ b/apps/api/src/finance/dto/vendor-bill.dto.ts @@ -0,0 +1,100 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsOptional, + IsString, + IsNotEmpty, + IsNumber, + IsArray, + ValidateNested, + Min, + IsDateString, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class VendorBillLineDto { + @ApiProperty({ description: '物料ID' }) + @IsString() + @IsNotEmpty() + materialId!: string; + + @ApiProperty({ description: '数量' }) + @IsNumber() + @Min(0.01) + quantity!: number; + + @ApiProperty({ description: '单价' }) + @IsNumber() + @Min(0) + unitPrice!: number; + + @ApiPropertyOptional({ + description: '税码ID(可选,未填使用发票级或默认税码)', + }) + @IsOptional() + @IsString() + taxCodeId?: string; + + @ApiPropertyOptional({ description: '借方过账科目ID' }) + @IsOptional() + @IsString() + accountId?: string; + + @ApiPropertyOptional({ description: '行描述' }) + @IsOptional() + @IsString() + description?: string; +} + +export class CreateVendorBillDto { + @ApiProperty({ description: '供应商ID' }) + @IsString() + @IsNotEmpty() + partnerId!: string; + + @ApiPropertyOptional({ description: '关联入库单ID(从入库单生成时填写)' }) + @IsOptional() + @IsString() + receiptId?: string; + + @ApiPropertyOptional({ description: '发票号码(外部供应商发票号)' }) + @IsOptional() + @IsString() + invoiceNo?: string; + + @ApiPropertyOptional({ description: '税码ID(发票级默认税码)' }) + @IsOptional() + @IsString() + taxCodeId?: string; + + @ApiProperty({ description: '应付到期日期 (ISO string)' }) + @IsDateString() + dueDate!: string; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + notes?: string; + + @ApiProperty({ description: '行项目列表', type: [VendorBillLineDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => VendorBillLineDto) + lines!: VendorBillLineDto[]; +} + +export class RecordVendorBillPaymentDto { + @ApiProperty({ description: '付款金额' }) + @IsNumber() + @Min(0.01) + amount!: number; + + @ApiProperty({ description: '付款方式: BANK_TRANSFER, CASH, CHECK' }) + @IsString() + @IsNotEmpty() + method!: string; + + @ApiPropertyOptional({ description: '付款备注' }) + @IsOptional() + @IsString() + notes?: string; +} diff --git a/apps/api/src/finance/finance-bridge.listener.ts b/apps/api/src/finance/finance-bridge.listener.ts index 0a6343f..d30644d 100644 --- a/apps/api/src/finance/finance-bridge.listener.ts +++ b/apps/api/src/finance/finance-bridge.listener.ts @@ -14,6 +14,16 @@ interface StockDepletedPayload { operatorId?: string; } +interface VendorBillPostedPayload { + companyId: string; + idempotencyKey?: string; + invoiceId: string; + taxCodeId?: string | null; + taxAccountId?: string | null; + taxRate?: number; + operatorId?: string; +} + interface InvoicePostedPayload { companyId: string; idempotencyKey?: string; @@ -109,4 +119,43 @@ export class FinanceBridgeListener { }); } } + + @OnEvent('finance.vendor_bill.posted') + async onVendorBillPosted(payload: VendorBillPostedPayload) { + try { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id: payload.invoiceId, companyId: payload.companyId }, + select: { invoiceNo: true }, + }); + + if (invoice?.invoiceNo) { + const existing = await this.prisma.journalEntry.findFirst({ + where: { + companyId: payload.companyId, + ref: invoice.invoiceNo, + journal: { code: 'PUR' }, + }, + select: { id: true }, + }); + if (existing) { + this.logger.debug( + `幂等跳过采购凭证: ref=${invoice.invoiceNo} 已存在 (id=${existing.id})`, + ); + return; + } + } + + await this.accountingService.postVendorBillPostedEntry(payload); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error(`采购发票过账事件记账失败: ${message}`); + await this.financeDlqService.recordFailure({ + eventName: 'finance.vendor_bill.posted', + idempotencyKey: payload.idempotencyKey, + payload: payload as unknown as Record, + error: message, + companyId: payload.companyId, + }); + } + } } diff --git a/apps/api/src/finance/finance.controller.spec.ts b/apps/api/src/finance/finance.controller.spec.ts index a41ee81..ca61d0f5 100644 --- a/apps/api/src/finance/finance.controller.spec.ts +++ b/apps/api/src/finance/finance.controller.spec.ts @@ -2,8 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FinanceController } from './finance.controller'; import { FinanceService } from './finance.service'; import { FinanceDlqService } from './finance-dlq.service'; +import { AccountingService } from './accounting.service'; import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; import { TenantGuard } from '../core/guards/tenant.guard'; +import { PermissionsGuard } from '../core/guards/permissions.guard'; describe('FinanceController', () => { let controller: FinanceController; @@ -13,6 +15,7 @@ describe('FinanceController', () => { getInvoices: jest.fn(), recordPayment: jest.fn(), postInvoice: jest.fn(), + getJournalEntries: jest.fn(), }; const mockFinanceDlqService = { @@ -20,18 +23,26 @@ describe('FinanceController', () => { retryPending: jest.fn(), }; + const mockAccountingService = { + reverseJournalEntry: jest.fn(), + getTrialBalance: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [FinanceController], providers: [ { provide: FinanceService, useValue: mockFinanceService }, { provide: FinanceDlqService, useValue: mockFinanceDlqService }, + { provide: AccountingService, useValue: mockAccountingService }, ], }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: () => true }) .overrideGuard(TenantGuard) .useValue({ canActivate: () => true }) + .overrideGuard(PermissionsGuard) + .useValue({ canActivate: () => true }) .compile(); controller = module.get(FinanceController); @@ -49,11 +60,7 @@ describe('FinanceController', () => { const result = await controller.createInvoice( 'c1', { id: 'u1', email: 'test@example.com' }, - { - orderId: 'o1', - amount: 1000, - dueDate: '2025-12-31', - }, + { orderId: 'o1', amount: 1000, dueDate: '2025-12-31' }, ); expect(result).toEqual(expected); @@ -91,21 +98,21 @@ describe('FinanceController', () => { }); describe('postInvoice', () => { - it('should post an invoice', async () => { + it('should post an invoice (no taxCodeId/taxRate param)', async () => { const expected = { id: 'inv1', postingStatus: 'POSTED' }; mockFinanceService.postInvoice.mockResolvedValue(expected); - const result = await controller.postInvoice('c1', { id: 'u1', email: 'test@example.com' }, 'inv1', { - taxRate: 0.13, - }); + const result = await controller.postInvoice( + 'c1', + { id: 'u1', email: 'test@example.com' }, + 'inv1', + ); expect(result).toEqual(expected); expect(mockFinanceService.postInvoice).toHaveBeenCalledWith( 'c1', 'inv1', 'u1', - undefined, - 0.13, ); }); }); @@ -133,4 +140,57 @@ describe('FinanceController', () => { expect(mockFinanceDlqService.retryPending).toHaveBeenCalledWith(10); }); }); + + describe('reverseJournalEntry', () => { + it('should reverse a journal entry', async () => { + const expected = { original: { id: 'je1' }, reversal: { id: 'je2' } }; + mockAccountingService.reverseJournalEntry.mockResolvedValue(expected); + + const result = await controller.reverseJournalEntry( + 'c1', + { id: 'u1', email: 'test@example.com' }, + 'je1', + { reason: '录入错误' }, + ); + + expect(result).toEqual(expected); + expect(mockAccountingService.reverseJournalEntry).toHaveBeenCalledWith( + 'c1', + 'je1', + 'u1', + '录入错误', + ); + }); + }); + + describe('getTrialBalance', () => { + it('should return trial balance', async () => { + const expected = { rows: [], summary: { isBalanced: true } }; + mockAccountingService.getTrialBalance.mockResolvedValue(expected); + + const result = await controller.getTrialBalance('c1'); + + expect(result).toEqual(expected); + expect(mockAccountingService.getTrialBalance).toHaveBeenCalledWith( + 'c1', + undefined, + undefined, + undefined, + ); + }); + }); + + describe('getJournalEntries', () => { + it('should return journal entries', async () => { + const expected = { data: [], total: 0 }; + mockFinanceService.getJournalEntries.mockResolvedValue(expected); + + const result = await controller.getJournalEntries('c1', { + page: 1, + limit: 20, + }); + + expect(result).toEqual(expected); + }); + }); }); diff --git a/apps/api/src/finance/finance.controller.ts b/apps/api/src/finance/finance.controller.ts index 4b942cd..34acb7e 100644 --- a/apps/api/src/finance/finance.controller.ts +++ b/apps/api/src/finance/finance.controller.ts @@ -7,29 +7,34 @@ import { UseGuards, Query, } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; import { FinanceService } from './finance.service'; import { FinanceDlqService } from './finance-dlq.service'; +import { AccountingService } from './accounting.service'; import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; import { TenantGuard } from '../core/guards/tenant.guard'; +import { PermissionsGuard } from '../core/guards/permissions.guard'; +import { RequirePermissions } from '../core/decorators/require-permissions.decorator'; import { CurrentCompany } from '../core/decorators/current-company.decorator'; import { CurrentUser } from '../core/decorators/current-user.decorator'; -import { - CreateInvoiceDto, - CreatePaymentDto, - PostInvoiceDto, -} from './dto/finance.dto'; +import { CreateInvoiceDto, CreatePaymentDto } from './dto/finance.dto'; import { PaginationDto } from '../core/dto/pagination.dto'; import type { JwtUserPayload } from '../core/http/request.types'; @ApiTags('财务管理 (Finance)') @ApiBearerAuth() -@UseGuards(JwtAuthGuard, TenantGuard) +@UseGuards(JwtAuthGuard, TenantGuard, PermissionsGuard) @Controller('finance') export class FinanceController { constructor( private readonly financeService: FinanceService, private readonly financeDlqService: FinanceDlqService, + private readonly accountingService: AccountingService, ) {} @Post('invoices') @@ -63,21 +68,74 @@ export class FinanceController { @Post('invoices/:id/post') @ApiOperation({ summary: '发票过账(触发收入凭证生成)' }) + @RequirePermissions('finance:post', 'finance:write') async postInvoice( @CurrentCompany() companyId: string, @CurrentUser() user: JwtUserPayload, @Param('id') invoiceId: string, - @Body() dto: PostInvoiceDto, ) { - return this.financeService.postInvoice( + return this.financeService.postInvoice(companyId, invoiceId, user.id); + } + + // ─── 凭证管理 ──────────────────────────────────── + + @Get('journal-entries') + @ApiOperation({ summary: '获取凭证列表' }) + @ApiQuery({ name: 'status', required: false, description: '过账状态过滤' }) + async getJournalEntries( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + @Query('status') status?: string, + ) { + return this.financeService.getJournalEntries(companyId, pagination, status); + } + + @Post('journal-entries/:id/reverse') + @ApiOperation({ + summary: '冲销凭证(生成反向借贷分录,原凭证标记 REVERSED)', + }) + @RequirePermissions('finance:reverse', 'finance:write') + async reverseJournalEntry( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') entryId: string, + @Body() body: { reason?: string }, + ) { + return this.accountingService.reverseJournalEntry( companyId, - invoiceId, + entryId, user.id, - dto.taxCodeId, - dto.taxRate, + body?.reason, + ); + } + + // ─── 试算平衡表 ────────────────────────────────── + + @Get('trial-balance') + @ApiOperation({ summary: '试算平衡表(按科目聚合已过账凭证借贷合计)' }) + @ApiQuery({ name: 'startDate', required: false, description: '起始日期 ISO' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 ISO' }) + @ApiQuery({ + name: 'accountType', + required: false, + description: 'ASSET/LIABILITY/EQUITY/REVENUE/EXPENSE', + }) + async getTrialBalance( + @CurrentCompany() companyId: string, + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + @Query('accountType') accountType?: string, + ) { + return this.accountingService.getTrialBalance( + companyId, + startDate ? new Date(startDate) : undefined, + endDate ? new Date(endDate) : undefined, + accountType, ); } + // ─── DLQ ───────────────────────────────────────── + @Get('dlq') @ApiOperation({ summary: '查看财务事件补偿队列' }) async getDlq(@Query('limit') limit?: string) { @@ -86,6 +144,7 @@ export class FinanceController { @Post('dlq/retry') @ApiOperation({ summary: '重试财务事件补偿队列' }) + @RequirePermissions('finance:admin') async retryDlq(@Body() body?: { limit?: number }) { return this.financeDlqService.retryPending(body?.limit ?? 20); } diff --git a/apps/api/src/finance/finance.module.ts b/apps/api/src/finance/finance.module.ts index 232e0cf..ded6ed0 100644 --- a/apps/api/src/finance/finance.module.ts +++ b/apps/api/src/finance/finance.module.ts @@ -1,6 +1,10 @@ import { Module } from '@nestjs/common'; import { FinanceController } from './finance.controller'; import { FinanceService } from './finance.service'; +import { TaxCodeController } from './tax-code.controller'; +import { VendorBillController } from './vendor-bill.controller'; +import { VendorBillService } from './vendor-bill.service'; +import { ThreeWayMatchService } from './three-way-match.service'; import { PrismaModule } from '../prisma/prisma.module'; import { AccountingService } from './accounting.service'; import { FinanceDlqService } from './finance-dlq.service'; @@ -9,13 +13,20 @@ import { EventQueueModule } from '../core/events/event-queue.module'; @Module({ imports: [PrismaModule, EventQueueModule], - controllers: [FinanceController], + controllers: [FinanceController, TaxCodeController, VendorBillController], providers: [ FinanceService, + VendorBillService, + ThreeWayMatchService, AccountingService, FinanceDlqService, FinanceBridgeListener, ], - exports: [AccountingService, FinanceDlqService], + exports: [ + AccountingService, + FinanceDlqService, + VendorBillService, + ThreeWayMatchService, + ], }) export class FinanceModule {} diff --git a/apps/api/src/finance/finance.service.spec.ts b/apps/api/src/finance/finance.service.spec.ts index 1e012ff..dffe9cf 100644 --- a/apps/api/src/finance/finance.service.spec.ts +++ b/apps/api/src/finance/finance.service.spec.ts @@ -1,4 +1,5 @@ import { NotFoundException } from '@nestjs/common'; +import { TaxNature } from '@prisma/client'; import { FinanceService } from './finance.service'; type MockPrisma = { @@ -13,6 +14,7 @@ type MockPrisma = { }; payment: { create: jest.Mock }; auditLog: { create: jest.Mock }; + account: { findFirst: jest.Mock }; $transaction: jest.Mock; }; @@ -21,6 +23,13 @@ type MockTx = { invoice: { update: jest.Mock }; }; +const mockTaxService = { + round2: jest.fn((v: number) => Math.round((v + Number.EPSILON) * 100) / 100), + resolveTaxCode: jest.fn(), + calcTaxFromTotal: jest.fn(), + getTaxAccountId: jest.fn(), +}; + describe('FinanceService', () => { const prisma: MockPrisma = { order: { findFirst: jest.fn() }, @@ -34,6 +43,7 @@ describe('FinanceService', () => { }, payment: { create: jest.fn() }, auditLog: { create: jest.fn() }, + account: { findFirst: jest.fn() }, $transaction: jest.fn(), }; @@ -56,6 +66,9 @@ describe('FinanceService', () => { eventEmitter as unknown as ConstructorParameters< typeof FinanceService >[1], + mockTaxService as unknown as ConstructorParameters< + typeof FinanceService + >[2], ); }); @@ -65,7 +78,30 @@ describe('FinanceService', () => { describe('createInvoice', () => { it('should create an invoice when order exists', async () => { - prisma.order.findFirst.mockResolvedValue({ id: 'o1', companyId: 'c1' }); + prisma.order.findFirst.mockResolvedValue({ + id: 'o1', + companyId: 'c1', + taxCodeId: null, + }); + mockTaxService.resolveTaxCode.mockResolvedValue({ + id: 'tc1', + code: 'VAT_13', + name: '增值税13%', + rate: 0.13, + isTaxInclusive: true, + taxNature: TaxNature.OUTPUT, + outputAccountId: null, + inputAccountId: null, + accountId: null, + isFallback: false, + }); + mockTaxService.calcTaxFromTotal.mockReturnValue({ + subTotal: 884.96, + taxAmount: 115.04, + total: 1000, + taxRate: 0.13, + taxNature: TaxNature.OUTPUT, + }); prisma.invoice.create.mockResolvedValue({ id: 'inv1', invoiceNo: 'INV-123', @@ -78,11 +114,7 @@ describe('FinanceService', () => { const result = await service.createInvoice( 'c1', - { - orderId: 'o1', - amount: 1000, - dueDate: '2025-12-31', - }, + { orderId: 'o1', amount: 1000, dueDate: '2025-12-31' }, 'u1', ); @@ -175,16 +207,19 @@ describe('FinanceService', () => { }); describe('postInvoice', () => { - it('should post an invoice and emit event', async () => { + it('should post an invoice using snapshot and emit event', async () => { prisma.invoice.findFirst.mockResolvedValue({ id: 'inv1', invoiceNo: 'INV-123', postingStatus: 'DRAFT', amount: 1000, - subTotal: 0, - taxAmount: 0, - taxCodeId: null, + subTotal: 884.96, + taxAmount: 115.04, + taxRate: 0.13, + taxNature: TaxNature.OUTPUT, + taxCodeId: 'tc1', order: { taxCodeId: null }, + taxCode: null, }); prisma.invoice.update.mockResolvedValue({ id: 'inv1', @@ -192,8 +227,19 @@ describe('FinanceService', () => { postingStatus: 'POSTED', }); prisma.auditLog.create.mockResolvedValue({}); + mockTaxService.resolveTaxCode.mockResolvedValue({ + id: 'tc1', + code: 'VAT_13', + rate: 0.13, + taxNature: TaxNature.OUTPUT, + outputAccountId: null, + inputAccountId: null, + accountId: null, + isFallback: false, + }); + mockTaxService.getTaxAccountId.mockReturnValue(null); - const result = await service.postInvoice('c1', 'inv1', 'u1', undefined, 0.13); + const result = await service.postInvoice('c1', 'inv1', 'u1'); expect(result.postingStatus).toBe('POSTED'); expect(prisma.invoice.update).toHaveBeenCalledWith({ @@ -205,8 +251,7 @@ describe('FinanceService', () => { expect.objectContaining({ companyId: 'c1', invoiceId: 'inv1', - taxCodeId: null, - taxRate: 0.13, + taxCodeId: 'tc1', }), ); }); diff --git a/apps/api/src/finance/finance.service.ts b/apps/api/src/finance/finance.service.ts index 576ed8a..ee9d8cc 100644 --- a/apps/api/src/finance/finance.service.ts +++ b/apps/api/src/finance/finance.service.ts @@ -1,12 +1,8 @@ -import { - BadRequestException, - Injectable, - Logger, - NotFoundException, -} from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { EntryPostingStatus, Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { TaxService } from '../core/tax/tax.service'; import { CreateInvoiceDto, CreatePaymentDto } from './dto/finance.dto'; import { PaginationDto } from '../core/dto/pagination.dto'; @@ -17,42 +13,9 @@ export class FinanceService { constructor( private readonly prisma: PrismaService, private readonly eventEmitter: EventEmitter2, + private readonly taxService: TaxService, ) {} - private round2(value: number) { - return Math.round((value + Number.EPSILON) * 100) / 100; - } - - private calcTaxFromTotal(total: number, taxRate: number) { - const safeRate = Math.max(0, Math.min(1, Number(taxRate ?? 0))); - const subTotal = this.round2(total / (1 + safeRate)); - const taxAmount = this.round2(total - subTotal); - return { subTotal, taxAmount, taxRate: safeRate }; - } - - private async resolveTaxCode(companyId: string, taxCodeId?: string | null) { - if (taxCodeId) { - const taxCode = await this.prisma.taxCode.findFirst({ - where: { id: taxCodeId, companyId, active: true }, - }); - if (!taxCode) { - throw new BadRequestException('税码不存在或已停用,请确认税码选择'); - } - return { ...taxCode, isFallback: false }; - } - - const defaultTaxCode = await this.prisma.taxCode.findFirst({ - where: { companyId, isDefault: true, active: true }, - orderBy: { updatedAt: 'desc' }, - }); - if (defaultTaxCode) { - return { ...defaultTaxCode, isFallback: false }; - } - - this.logger.warn(`未配置默认税码,发票将使用 13% 默认税率: ${companyId}`); - return { id: null, rate: 0.13, isFallback: true }; - } - async createInvoice( companyId: string, dto: CreateInvoiceDto, @@ -64,21 +27,28 @@ export class FinanceService { }); if (!order) throw new NotFoundException('找不到销售订单'); - const resolvedTaxCode = await this.resolveTaxCode( + const resolvedTaxCode = await this.taxService.resolveTaxCode( companyId, dto.taxCodeId ?? order.taxCodeId, + { operatorId, entity: 'Invoice', entityId: 'new' }, ); - const amount = this.round2(Number(dto.amount)); - const breakdown = this.calcTaxFromTotal(amount, resolvedTaxCode.rate); + const amount = this.taxService.round2(Number(dto.amount)); + const breakdown = this.taxService.calcTaxFromTotal( + amount, + resolvedTaxCode.rate, + resolvedTaxCode.taxNature, + ); const invoice = await this.prisma.invoice.create({ data: { - invoiceNo: `INV-${Date.now()}`, + invoiceNo: 'INV-' + Date.now(), orderId: dto.orderId, amount, subTotal: breakdown.subTotal, taxAmount: breakdown.taxAmount, + taxRate: breakdown.taxRate, + taxNature: breakdown.taxNature, taxCodeId: resolvedTaxCode.id ?? null, dueDate: new Date(dto.dueDate), status: 'UNPAID', @@ -87,7 +57,6 @@ export class FinanceService { }, }); - // 记录审计日志 await this.prisma.auditLog.create({ data: { userId: operatorId, @@ -98,8 +67,11 @@ export class FinanceService { amount, orderId: dto.orderId, taxCodeId: resolvedTaxCode.id ?? null, - taxRate: resolvedTaxCode.rate ?? 0, - taxFallback: resolvedTaxCode.isFallback ?? false, + taxRate: breakdown.taxRate, + taxNature: breakdown.taxNature, + subTotal: breakdown.subTotal, + taxAmount: breakdown.taxAmount, + taxFallback: resolvedTaxCode.isFallback, }, companyId, }, @@ -115,7 +87,7 @@ export class FinanceService { const [data, total] = await Promise.all([ this.prisma.invoice.findMany({ where, - include: { order: true, payments: true }, + include: { order: true, payments: true, taxCode: true }, orderBy: { issuedDate: 'desc' }, skip: (page - 1) * limit, take: limit, @@ -147,10 +119,10 @@ export class FinanceService { }); const totalPaid = - inv.payments.reduce((sum, p) => sum + p.amount, 0) + dto.amount; + inv.payments.reduce((sum, p) => sum + Number(p.amount), 0) + dto.amount; let newStatus = inv.status; - if (totalPaid >= inv.amount) newStatus = 'PAID'; + if (totalPaid >= Number(inv.amount)) newStatus = 'PAID'; else if (totalPaid > 0) newStatus = 'PARTIAL'; await tx.invoice.update({ @@ -162,16 +134,17 @@ export class FinanceService { }); } - async postInvoice( - companyId: string, - invoiceId: string, - operatorId: string, - taxCodeId?: string, - taxRate?: number, - ) { + /** + * 过账发票 — 使用保存在发票上的价税快照,禁止重新按当前税率覆盖历史金额。 + * 只有在 subTotal/taxAmount 均为 0 的历史遗留数据上才做一次性的兜底计算。 + */ + async postInvoice(companyId: string, invoiceId: string, operatorId: string) { const invoice = await this.prisma.invoice.findFirst({ where: { id: invoiceId, companyId }, - include: { order: { select: { taxCodeId: true } } }, + include: { + order: { select: { taxCodeId: true } }, + taxCode: true, + }, }); if (!invoice) { throw new NotFoundException('发票不存在'); @@ -186,20 +159,29 @@ export class FinanceService { }; } - const resolvedTaxCode = await this.resolveTaxCode( - companyId, - taxCodeId ?? invoice.taxCodeId ?? invoice.order?.taxCodeId, - ); - const amount = this.round2(Number(invoice.amount)); - let subTotal = this.round2(Number(invoice.subTotal ?? 0)); - let taxAmount = this.round2(Number(invoice.taxAmount ?? 0)); + const amount = this.taxService.round2(Number(invoice.amount)); + let subTotal = this.taxService.round2(Number(invoice.subTotal ?? 0)); + let taxAmount = this.taxService.round2(Number(invoice.taxAmount ?? 0)); const shouldRecalc = subTotal <= 0 && taxAmount <= 0 && amount > 0; if (shouldRecalc) { - const fallbackRate = taxRate ?? resolvedTaxCode.rate ?? 0.13; - const breakdown = this.calcTaxFromTotal(amount, fallbackRate); + const fallbackRate = + Number(invoice.taxRate) > 0 + ? Number(invoice.taxRate) + : Number(invoice.taxCode?.rate ?? 0.13); + const breakdown = this.taxService.calcTaxFromTotal( + amount, + fallbackRate, + invoice.taxNature, + ); subTotal = breakdown.subTotal; taxAmount = breakdown.taxAmount; + this.logger.warn( + '发票缺少价税快照,过账时兜底计算: invoiceNo=' + + invoice.invoiceNo + + ' rate=' + + fallbackRate, + ); } const updateData: Prisma.InvoiceUpdateInput = { @@ -209,8 +191,8 @@ export class FinanceService { updateData.subTotal = subTotal; updateData.taxAmount = taxAmount; } - if (!invoice.taxCodeId && resolvedTaxCode.id) { - updateData.taxCode = { connect: { id: resolvedTaxCode.id } }; + if (!invoice.taxCodeId && invoice.order?.taxCodeId) { + updateData.taxCode = { connect: { id: invoice.order.taxCodeId } }; } const updated = await this.prisma.invoice.update({ @@ -218,6 +200,16 @@ export class FinanceService { data: updateData, }); + const taxCodeId = invoice.taxCodeId ?? invoice.order?.taxCodeId ?? null; + let taxAccountId: string | null = null; + if (taxCodeId) { + const resolved = await this.taxService.resolveTaxCode( + companyId, + taxCodeId, + ); + taxAccountId = this.taxService.getTaxAccountId(resolved); + } + await this.prisma.auditLog.create({ data: { userId: operatorId, @@ -226,9 +218,11 @@ export class FinanceService { entityId: invoice.id, details: { invoiceNo: invoice.invoiceNo, - taxCodeId: invoice.taxCodeId ?? resolvedTaxCode.id ?? null, - taxRate: taxRate ?? resolvedTaxCode.rate ?? 0.13, - taxFallback: resolvedTaxCode.isFallback ?? false, + taxCodeId, + taxRate: invoice.taxRate ?? 0.13, + taxNature: invoice.taxNature, + taxAccountId, + usedSnapshot: !shouldRecalc, }, companyId, }, @@ -236,13 +230,53 @@ export class FinanceService { this.eventEmitter.emit('finance.invoice.posted', { companyId, - idempotencyKey: `invoice_posted:${invoice.id}`, + idempotencyKey: 'invoice_posted:' + invoice.id, invoiceId: invoice.id, - taxCodeId: invoice.taxCodeId ?? resolvedTaxCode.id ?? null, - taxRate: taxRate ?? resolvedTaxCode.rate ?? 0.13, + taxCodeId, + taxAccountId, + taxRate: invoice.taxRate ?? 0.13, operatorId, }); return updated; } + + async getJournalEntries( + companyId: string, + pagination: PaginationDto, + status?: string, + ) { + const { page = 1, limit = 20 } = pagination; + const where: { companyId: string; postingStatus?: EntryPostingStatus } = { + companyId, + }; + if ( + status && + Object.values(EntryPostingStatus).includes(status as EntryPostingStatus) + ) { + where.postingStatus = status as EntryPostingStatus; + } + + const [data, total] = await Promise.all([ + this.prisma.journalEntry.findMany({ + where, + include: { + journal: { select: { id: true, code: true, name: true, type: true } }, + lines: { + include: { + account: true, + partner: { select: { id: true, name: true } }, + }, + orderBy: { lineNo: 'asc' }, + }, + }, + orderBy: { date: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.journalEntry.count({ where }), + ]); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } } diff --git a/apps/api/src/finance/tax-code.controller.spec.ts b/apps/api/src/finance/tax-code.controller.spec.ts new file mode 100644 index 0000000..f2722a0 --- /dev/null +++ b/apps/api/src/finance/tax-code.controller.spec.ts @@ -0,0 +1,82 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; +import { TaxCodeController } from './tax-code.controller'; +import { PrismaService } from '../prisma/prisma.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; + +describe('TaxCodeController', () => { + let controller: TaxCodeController; + + const mockPrisma = { + taxCode: { + create: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + updateMany: jest.fn(), + }, + }; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + controllers: [TaxCodeController], + providers: [{ provide: PrismaService, useValue: mockPrisma }], + }) + .overrideGuard(JwtAuthGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(TenantGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(TaxCodeController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('create', () => { + it('should create a tax code', async () => { + mockPrisma.taxCode.findFirst.mockResolvedValue(null); + mockPrisma.taxCode.create.mockResolvedValue({ + id: 'tc1', + code: 'VAT_13', + name: '增值税13%', + }); + + const result = await controller.create('c1', { + code: 'VAT_13', + name: '增值税13%', + rate: 0.13, + }); + + expect(result.id).toBe('tc1'); + expect(mockPrisma.taxCode.create).toHaveBeenCalled(); + }); + + it('should throw when code already exists', async () => { + mockPrisma.taxCode.findFirst.mockResolvedValue({ id: 'existing' }); + + await expect( + controller.create('c1', { + code: 'VAT_13', + name: '增值税13%', + rate: 0.13, + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + }); + + describe('list', () => { + it('should return tax codes', async () => { + mockPrisma.taxCode.findMany.mockResolvedValue([ + { id: 'tc1', code: 'VAT_13' }, + ]); + + const result = await controller.list('c1'); + expect(result).toHaveLength(1); + }); + }); +}); diff --git a/apps/api/src/finance/tax-code.controller.ts b/apps/api/src/finance/tax-code.controller.ts new file mode 100644 index 0000000..318ae20 --- /dev/null +++ b/apps/api/src/finance/tax-code.controller.ts @@ -0,0 +1,145 @@ +import { + Controller, + Get, + Post, + Put, + Body, + Param, + UseGuards, + Query, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { TaxNature, Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; +import { CurrentCompany } from '../core/decorators/current-company.decorator'; +import { CreateTaxCodeDto, UpdateTaxCodeDto } from './dto/tax-code.dto'; +import { BadRequestException } from '@nestjs/common'; + +@ApiTags('税码管理 (TaxCode)') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, TenantGuard) +@Controller('finance/tax-codes') +export class TaxCodeController { + constructor(private readonly prisma: PrismaService) {} + + @Post() + @ApiOperation({ summary: '创建税码' }) + async create( + @CurrentCompany() companyId: string, + @Body() dto: CreateTaxCodeDto, + ) { + const existing = await this.prisma.taxCode.findFirst({ + where: { companyId, code: dto.code }, + }); + if (existing) { + throw new BadRequestException(`税码编码 ${dto.code} 已存在`); + } + + if (dto.isDefault) { + await this.prisma.taxCode.updateMany({ + where: { companyId, isDefault: true }, + data: { isDefault: false }, + }); + } + + return this.prisma.taxCode.create({ + data: { + code: dto.code, + name: dto.name, + rate: dto.rate, + isTaxInclusive: dto.isTaxInclusive ?? true, + isDefault: dto.isDefault ?? false, + active: true, + taxNature: dto.taxNature ?? TaxNature.OUTPUT, + accountId: dto.accountId ?? null, + outputAccountId: dto.outputAccountId ?? null, + inputAccountId: dto.inputAccountId ?? null, + companyId, + }, + }); + } + + @Get() + @ApiOperation({ summary: '获取税码列表' }) + async list( + @CurrentCompany() companyId: string, + @Query('active') active?: string, + ) { + const where: Prisma.TaxCodeWhereInput = { companyId }; + if (active !== undefined) { + where.active = active === 'true'; + } + return this.prisma.taxCode.findMany({ + where, + include: { + account: { select: { id: true, code: true, name: true } }, + outputAccount: { select: { id: true, code: true, name: true } }, + inputAccount: { select: { id: true, code: true, name: true } }, + }, + orderBy: [{ isDefault: 'desc' }, { code: 'asc' }], + }); + } + + @Get(':id') + @ApiOperation({ summary: '获取税码详情' }) + async getById(@CurrentCompany() companyId: string, @Param('id') id: string) { + const taxCode = await this.prisma.taxCode.findFirst({ + where: { id, companyId }, + include: { + account: true, + outputAccount: true, + inputAccount: true, + }, + }); + if (!taxCode) throw new BadRequestException('税码不存在'); + return taxCode; + } + + @Put(':id') + @ApiOperation({ summary: '更新税码' }) + async update( + @CurrentCompany() companyId: string, + @Param('id') id: string, + @Body() dto: UpdateTaxCodeDto, + ) { + const existing = await this.prisma.taxCode.findFirst({ + where: { id, companyId }, + }); + if (!existing) throw new BadRequestException('税码不存在'); + + if (dto.isDefault && !existing.isDefault) { + await this.prisma.taxCode.updateMany({ + where: { companyId, isDefault: true }, + data: { isDefault: false }, + }); + } + + const updateData: Prisma.TaxCodeUpdateInput = {}; + if (dto.name !== undefined) updateData.name = dto.name; + if (dto.rate !== undefined) updateData.rate = dto.rate; + if (dto.isTaxInclusive !== undefined) + updateData.isTaxInclusive = dto.isTaxInclusive; + if (dto.isDefault !== undefined) updateData.isDefault = dto.isDefault; + if (dto.active !== undefined) updateData.active = dto.active; + if (dto.taxNature !== undefined) updateData.taxNature = dto.taxNature; + if (dto.accountId !== undefined) + updateData.account = dto.accountId + ? { connect: { id: dto.accountId } } + : { disconnect: true }; + if (dto.outputAccountId !== undefined) + updateData.outputAccount = dto.outputAccountId + ? { connect: { id: dto.outputAccountId } } + : { disconnect: true }; + if (dto.inputAccountId !== undefined) + updateData.inputAccount = dto.inputAccountId + ? { connect: { id: dto.inputAccountId } } + : { disconnect: true }; + + return this.prisma.taxCode.update({ + where: { id }, + data: updateData, + }); + } +} diff --git a/apps/api/src/finance/three-way-match.service.spec.ts b/apps/api/src/finance/three-way-match.service.spec.ts new file mode 100644 index 0000000..233ffb1 --- /dev/null +++ b/apps/api/src/finance/three-way-match.service.spec.ts @@ -0,0 +1,261 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { ThreeWayMatchStatus } from '@prisma/client'; +import { ThreeWayMatchService } from './three-way-match.service'; +import { PrismaService } from '../prisma/prisma.service'; + +const MOCK_COMPANY = 'company-1'; +const MOCK_INVOICE_ID = 'inv-1'; +const MOCK_OPERATOR = 'user-1'; + +function makeMockPoLine(overrides: Record = {}) { + return { + id: 'pol-1', + orderId: 'po-1', + lineNo: 1, + productId: null, + materialId: 'mat-1', + description: null, + quantity: 100, + receivedQuantity: 100, + unitPrice: 10, + taxRate: 0.13, + taxAmount: 130, + subTotal: 1000, + totalAmount: 1130, + companyId: MOCK_COMPANY, + uom: 'pcs', + material: { id: 'mat-1', name: 'Material A' }, + ...overrides, + }; +} + +function makeMockGrLine(overrides: Record = {}) { + return { + id: 'grl-1', + receiptId: 'gr-1', + productId: 'prod-1', + materialId: 'mat-1', + locationId: 'loc-1', + quantity: 100, + batchNo: 'BATCH1', + ...overrides, + }; +} + +function makeMockBillLine(overrides: Record = {}) { + return { + id: 'bil-1', + invoiceId: MOCK_INVOICE_ID, + materialId: 'mat-1', + quantity: 100, + unitPrice: 10, + lineTotal: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxCodeId: null, + description: null, + material: { id: 'mat-1', name: 'Material A', sku: 'MAT-A' }, + ...overrides, + }; +} + +function makeMockInvoice(linesOverride?: unknown[]) { + return { + id: MOCK_INVOICE_ID, + invoiceNo: 'VB-12345', + partnerId: 'partner-1', + receiptId: 'gr-1', + amount: 1130, + subTotal: 1000, + taxAmount: 130, + taxRate: 0.13, + taxCodeId: null, + taxNature: 'INPUT', + status: 'UNPAID', + postingStatus: 'DRAFT', + companyId: MOCK_COMPANY, + dueDate: new Date(), + issuedDate: new Date(), + notes: null, + matchStatus: 'NOT_APPLICABLE', + lines: linesOverride ?? [makeMockBillLine()], + receipt: { + id: 'gr-1', + receiptNo: 'GR-001', + orderId: 'po-1', + partnerId: 'partner-1', + status: 'CONFIRMED', + receiptDate: new Date(), + notes: null, + companyId: MOCK_COMPANY, + lines: [makeMockGrLine()], + order: { + id: 'po-1', + orderNo: 'PO-001', + partnerId: 'partner-1', + status: 'RECEIVED', + orderDate: new Date(), + expectedDate: null, + notes: null, + subTotal: 1000, + taxTotal: 130, + totalAmount: 1130, + companyId: MOCK_COMPANY, + lines: [makeMockPoLine()], + }, + }, + }; +} + +describe('ThreeWayMatchService', () => { + let service: ThreeWayMatchService; + let prisma: { + purchaseInvoice: { + findFirst: jest.Mock; + update: jest.Mock; + }; + auditLog: { + create: jest.Mock; + }; + }; + + beforeEach(async () => { + prisma = { + purchaseInvoice: { findFirst: jest.fn(), update: jest.fn() }, + auditLog: { create: jest.fn() }, + }; + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ThreeWayMatchService, + { provide: PrismaService, useValue: prisma }, + ], + }).compile(); + service = module.get(ThreeWayMatchService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('validateMatch', () => { + it('should return NOT_APPLICABLE when no receiptId', async () => { + const invoice = { ...makeMockInvoice(), receiptId: null, receipt: null }; + prisma.purchaseInvoice.findFirst.mockResolvedValue(invoice); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect(result.status).toBe(ThreeWayMatchStatus.NOT_APPLICABLE); + expect(result.overallMatched).toBe(true); + }); + it('should return MATCHED when quantities and amounts match', async () => { + prisma.purchaseInvoice.findFirst.mockResolvedValue(makeMockInvoice()); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect(result.status).toBe(ThreeWayMatchStatus.MATCHED); + expect(result.lines).toHaveLength(1); + expect(result.lines[0].matched).toBe(true); + }); + it('should detect bill qty exceeding PO qty', async () => { + const billLine = makeMockBillLine({ + quantity: 150, + lineTotal: 1695, + taxAmount: 195, + subTotal: 1500, + }); + prisma.purchaseInvoice.findFirst.mockResolvedValue( + makeMockInvoice([billLine]), + ); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect(result.status).toBe(ThreeWayMatchStatus.MISMATCH); + expect( + result.lines[0].reasons.some((r: string) => + r.includes('超过PO采购数量'), + ), + ).toBe(true); + }); + it('should detect GR qty exceeding PO qty', async () => { + const invoice = makeMockInvoice(); + invoice.receipt.lines = [makeMockGrLine({ quantity: 150 })]; + prisma.purchaseInvoice.findFirst.mockResolvedValue(invoice); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect( + result.lines[0].reasons.some((r: string) => r.includes('GR收货数量')), + ).toBe(true); + }); + it('should detect amount mismatch', async () => { + const billLine = makeMockBillLine({ + lineTotal: 1500, + taxAmount: 170, + subTotal: 1330, + }); + prisma.purchaseInvoice.findFirst.mockResolvedValue( + makeMockInvoice([billLine]), + ); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect(result.status).toBe(ThreeWayMatchStatus.MISMATCH); + }); + it('should detect missing PO line in invoice', async () => { + prisma.purchaseInvoice.findFirst.mockResolvedValue(makeMockInvoice([])); + const result = await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID); + expect(result.status).toBe(ThreeWayMatchStatus.MISMATCH); + }); + it('should throw NotFoundException when invoice not found', async () => { + prisma.purchaseInvoice.findFirst.mockResolvedValue(null); + await expect( + service.validateMatch(MOCK_COMPANY, 'nonexistent'), + ).rejects.toThrow(NotFoundException); + }); + it('should respect custom tolerance', async () => { + const billLine = makeMockBillLine({ + lineTotal: 1150, + subTotal: 1018, + taxAmount: 132, + }); + prisma.purchaseInvoice.findFirst.mockResolvedValue( + makeMockInvoice([billLine]), + ); + expect( + (await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID, 0.05, 0.01)) + .lines[0].matched, + ).toBe(true); + expect( + (await service.validateMatch(MOCK_COMPANY, MOCK_INVOICE_ID, 0.01, 0.01)) + .lines[0].matched, + ).toBe(false); + }); + }); + + describe('validateAndPersist', () => { + it('should persist MATCHED status and audit log', async () => { + prisma.purchaseInvoice.findFirst.mockResolvedValue(makeMockInvoice()); + prisma.purchaseInvoice.update.mockResolvedValue({}); + prisma.auditLog.create.mockResolvedValue({}); + const result = await service.validateAndPersist( + MOCK_COMPANY, + MOCK_INVOICE_ID, + MOCK_OPERATOR, + ); + expect(result.status).toBe(ThreeWayMatchStatus.MATCHED); + expect(prisma.auditLog.create).toHaveBeenCalled(); + }); + it('should throw BadRequestException on mismatch', async () => { + const billLine = makeMockBillLine({ + quantity: 200, + lineTotal: 2260, + taxAmount: 260, + subTotal: 2000, + }); + prisma.purchaseInvoice.findFirst.mockResolvedValue( + makeMockInvoice([billLine]), + ); + prisma.purchaseInvoice.update.mockResolvedValue({}); + prisma.auditLog.create.mockResolvedValue({}); + await expect( + service.validateAndPersist( + MOCK_COMPANY, + MOCK_INVOICE_ID, + MOCK_OPERATOR, + ), + ).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/api/src/finance/three-way-match.service.ts b/apps/api/src/finance/three-way-match.service.ts new file mode 100644 index 0000000..c8dc78b --- /dev/null +++ b/apps/api/src/finance/three-way-match.service.ts @@ -0,0 +1,316 @@ +import { + Injectable, + Logger, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { ThreeWayMatchStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; + +const DEFAULT_TOLERANCE_RATE = 0.05; +const DEFAULT_TOLERANCE_ABS = 0.01; + +export interface MatchLineDetail { + materialId: string; + materialName?: string; + poQuantity: number; + poUnitPrice: number; + poSubTotal: number; + poTaxAmount: number; + poTotalAmount: number; + grQuantity: number; + billQuantity: number; + billUnitPrice: number; + billSubTotal: number; + billTaxAmount: number; + billTotalAmount: number; + quantityDiff: number; + amountDiff: number; + taxDiff: number; + matched: boolean; + reasons: string[]; +} + +export interface ThreeWayMatchResult { + invoiceId: string; + invoiceNo: string; + status: ThreeWayMatchStatus; + overallMatched: boolean; + lines: MatchLineDetail[]; + summary: { + totalPoAmount: number; + totalGrQuantity: number; + totalBillAmount: number; + totalBillTax: number; + amountDifference: number; + taxDifference: number; + mismatchedLineCount: number; + totalLineCount: number; + }; + checkedAt: Date; +} + +@Injectable() +export class ThreeWayMatchService { + private readonly logger = new Logger(ThreeWayMatchService.name); + + constructor(private readonly prisma: PrismaService) {} + + private round2(value: number): number { + return Math.round((value + Number.EPSILON) * 100) / 100; + } + + private buildNotApplicableResult( + invoiceId: string, + invoiceNo: string, + ): ThreeWayMatchResult { + return { + invoiceId, + invoiceNo, + status: ThreeWayMatchStatus.NOT_APPLICABLE, + overallMatched: true, + lines: [], + summary: { + totalPoAmount: 0, + totalGrQuantity: 0, + totalBillAmount: 0, + totalBillTax: 0, + amountDifference: 0, + taxDifference: 0, + mismatchedLineCount: 0, + totalLineCount: 0, + }, + checkedAt: new Date(), + }; + } + + async validateMatch( + companyId: string, + invoiceId: string, + toleranceRate = DEFAULT_TOLERANCE_RATE, + toleranceAbs = DEFAULT_TOLERANCE_ABS, + ): Promise { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id: invoiceId, companyId }, + include: { + lines: { + include: { + material: { select: { id: true, name: true, sku: true } }, + }, + }, + receipt: { + include: { + lines: true, + order: { + include: { + lines: { + include: { material: { select: { id: true, name: true } } }, + }, + }, + }, + }, + }, + }, + }); + if (!invoice) throw new NotFoundException('采购发票不存在'); + if (!invoice.receiptId || !invoice.receipt) { + this.logger.warn(`发票 ${invoice.invoiceNo} 未关联入库单,跳过三单匹配`); + return this.buildNotApplicableResult(invoice.id, invoice.invoiceNo); + } + const receipt = invoice.receipt; + const po = receipt.order; + if (!po) { + this.logger.warn( + `入库单 ${receipt.receiptNo} 未关联采购订单,跳过三单匹配`, + ); + return this.buildNotApplicableResult(invoice.id, invoice.invoiceNo); + } + const poLinesByMat = new Map(); + for (const pl of po.lines) { + if (!pl.materialId) continue; + if (!poLinesByMat.has(pl.materialId)) poLinesByMat.set(pl.materialId, []); + poLinesByMat.get(pl.materialId)!.push(pl); + } + const grQtyByMat = new Map(); + for (const gl of receipt.lines) { + grQtyByMat.set( + gl.materialId, + (grQtyByMat.get(gl.materialId) ?? 0) + gl.quantity, + ); + } + const lineDetails: MatchLineDetail[] = []; + let totalPoAmount = 0, + totalGrQty = 0, + totalBillAmount = 0, + totalBillTax = 0; + for (const bl of invoice.lines) { + const matId = bl.materialId; + const poLines = poLinesByMat.get(matId); + let poQty = 0, + poPrice = 0, + poSub = 0, + poTax = 0, + poTotal = 0; + if (poLines?.length) { + for (const pl of poLines) { + poQty += pl.quantity; + poPrice = Number(pl.unitPrice); + poSub += Number(pl.subTotal); + poTax += Number(pl.taxAmount); + poTotal += Number(pl.totalAmount); + } + } + const grQty = grQtyByMat.get(matId) ?? 0; + const qtyDiff = bl.quantity - poQty; + const amtDiff = this.round2(Number(bl.lineTotal) - poTotal); + const txDiff = this.round2(Number(bl.taxAmount) - poTax); + totalPoAmount += poTotal; + totalGrQty += grQty; + totalBillAmount += Number(bl.lineTotal); + totalBillTax += Number(bl.taxAmount); + const reasons: string[] = []; + if (grQty > poQty + toleranceAbs) + reasons.push(`GR收货数量(${grQty})超过PO采购数量(${poQty})`); + if (bl.quantity > poQty + toleranceAbs) + reasons.push(`开票数量(${bl.quantity})超过PO采购数量(${poQty})`); + if ( + poTotal > 0 && + Math.abs(amtDiff) > toleranceAbs && + Math.abs(amtDiff / poTotal) > toleranceRate + ) + reasons.push( + `开票金额(${String(bl.lineTotal)})与PO金额(${poTotal})差异超限: ${this.round2(Math.abs(amtDiff / poTotal) * 100)}%`, + ); + if ( + poTax > 0 && + Math.abs(txDiff) > toleranceAbs && + Math.abs(txDiff / poTax) > toleranceRate + ) + reasons.push( + `开票税额(${String(bl.taxAmount)})与PO税额(${poTax})差异超限: ${this.round2(Math.abs(txDiff / poTax) * 100)}%`, + ); + lineDetails.push({ + materialId: matId, + materialName: bl.material?.name, + poQuantity: poQty, + poUnitPrice: poPrice, + poSubTotal: this.round2(poSub), + poTaxAmount: this.round2(poTax), + poTotalAmount: this.round2(poTotal), + grQuantity: grQty, + billQuantity: bl.quantity, + billUnitPrice: Number(bl.unitPrice), + billSubTotal: this.round2(Number(bl.subTotal)), + billTaxAmount: this.round2(Number(bl.taxAmount)), + billTotalAmount: this.round2(Number(bl.lineTotal)), + quantityDiff: this.round2(qtyDiff), + amountDiff: this.round2(amtDiff), + taxDiff: this.round2(txDiff), + matched: reasons.length === 0, + reasons, + }); + } + for (const [matId, pls] of poLinesByMat) { + if (invoice.lines.some((l) => l.materialId === matId)) continue; + const pQty = pls.reduce((s, l) => s + l.quantity, 0); + const pAmt = pls.reduce((s, l) => s + Number(l.totalAmount), 0); + if (pQty > 0 && pAmt > 0) { + lineDetails.push({ + materialId: matId, + materialName: pls[0]?.material?.name, + poQuantity: pQty, + poUnitPrice: Number(pls[0]?.unitPrice ?? 0), + poSubTotal: this.round2( + pls.reduce((s, l) => s + Number(l.subTotal), 0), + ), + poTaxAmount: this.round2( + pls.reduce((s, l) => s + Number(l.taxAmount), 0), + ), + poTotalAmount: this.round2(pAmt), + grQuantity: grQtyByMat.get(matId) ?? 0, + billQuantity: 0, + billUnitPrice: 0, + billSubTotal: 0, + billTaxAmount: 0, + billTotalAmount: 0, + quantityDiff: this.round2(-pQty), + amountDiff: this.round2(-pAmt), + taxDiff: this.round2( + -pls.reduce((s, l) => s + Number(l.taxAmount), 0), + ), + matched: false, + reasons: [`PO行项(物料${matId})在发票中缺失`], + }); + } + } + const mismatched = lineDetails.filter((l) => !l.matched).length; + const overall = mismatched === 0; + return { + invoiceId: invoice.id, + invoiceNo: invoice.invoiceNo, + status: overall + ? ThreeWayMatchStatus.MATCHED + : ThreeWayMatchStatus.MISMATCH, + overallMatched: overall, + lines: lineDetails, + summary: { + totalPoAmount: this.round2(totalPoAmount), + totalGrQuantity: this.round2(totalGrQty), + totalBillAmount: this.round2(totalBillAmount), + totalBillTax: this.round2(totalBillTax), + amountDifference: this.round2(totalBillAmount - totalPoAmount), + taxDifference: this.round2( + totalBillTax - lineDetails.reduce((s, l) => s + l.poTaxAmount, 0), + ), + mismatchedLineCount: mismatched, + totalLineCount: lineDetails.length, + }, + checkedAt: new Date(), + }; + } + + async validateAndPersist( + companyId: string, + invoiceId: string, + operatorId: string, + toleranceRate?: number, + toleranceAbs?: number, + ): Promise { + const result = await this.validateMatch( + companyId, + invoiceId, + toleranceRate, + toleranceAbs, + ); + await this.prisma.purchaseInvoice.update({ + where: { id: invoiceId }, + data: { matchStatus: result.status }, + }); + await this.prisma.auditLog.create({ + data: { + userId: operatorId, + action: 'THREE_WAY_MATCH', + entity: 'PurchaseInvoice', + entityId: invoiceId, + details: { + status: result.status, + overallMatched: result.overallMatched, + summary: result.summary, + mismatchedLines: result.lines + .filter((l) => !l.matched) + .map((l) => ({ materialId: l.materialId, reasons: l.reasons })), + }, + companyId, + }, + }); + if (!result.overallMatched) { + const mismatchedReasons = result.lines + .filter((l) => !l.matched) + .flatMap((l) => l.reasons); + throw new BadRequestException( + `三单匹配失败,无法过账。差异明细:\n${mismatchedReasons.join('\n')}`, + ); + } + return result; + } +} diff --git a/apps/api/src/finance/vendor-bill.controller.ts b/apps/api/src/finance/vendor-bill.controller.ts new file mode 100644 index 0000000..4957d44 --- /dev/null +++ b/apps/api/src/finance/vendor-bill.controller.ts @@ -0,0 +1,98 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Query, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { VendorBillService } from './vendor-bill.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; +import { CurrentCompany } from '../core/decorators/current-company.decorator'; +import { CurrentUser } from '../core/decorators/current-user.decorator'; +import { + CreateVendorBillDto, + RecordVendorBillPaymentDto, +} from './dto/vendor-bill.dto'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import type { JwtUserPayload } from '../core/http/request.types'; +import { ThreeWayMatchService } from './three-way-match.service'; + +@ApiTags('应付管理 (Vendor Bill)') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, TenantGuard) +@Controller('finance/vendor-bills') +export class VendorBillController { + constructor( + private readonly vendorBillService: VendorBillService, + private readonly threeWayMatchService: ThreeWayMatchService, + ) {} + + @Post() + @ApiOperation({ summary: '创建采购发票(应付单)' }) + async create( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Body() dto: CreateVendorBillDto, + ) { + return this.vendorBillService.create(companyId, dto, user.id); + } + + @Get() + @ApiOperation({ summary: '获取采购发票列表' }) + async findAll( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + ) { + return this.vendorBillService.findAll(companyId, pagination); + } + + @Get(':id') + @ApiOperation({ summary: '获取采购发票详情' }) + async findOne(@CurrentCompany() companyId: string, @Param('id') id: string) { + return this.vendorBillService.findOne(companyId, id); + } + + @Post(':id/confirm') + @ApiOperation({ summary: '确认应付单(草稿→待付)' }) + async confirm( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') id: string, + ) { + return this.vendorBillService.confirm(companyId, id, user.id); + } + + @Post(':id/payments') + @ApiOperation({ summary: '登记应付付款' }) + async recordPayment( + @CurrentCompany() companyId: string, + @Param('id') invoiceId: string, + @Body() dto: RecordVendorBillPaymentDto, + ) { + return this.vendorBillService.recordPayment(companyId, invoiceId, dto); + } + + @Post(':id/post') + @ApiOperation({ summary: '过账采购发票(触发应付凭证生成)' }) + async post( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') id: string, + ) { + return this.vendorBillService.post(companyId, id, user.id); + } + + @Post(':id/match') + @ApiOperation({ summary: '执行三单匹配校验 (PO↔GR↔Bill)' }) + async match( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') id: string, + ) { + return this.threeWayMatchService.validateAndPersist(companyId, id, user.id); + } +} diff --git a/apps/api/src/finance/vendor-bill.service.ts b/apps/api/src/finance/vendor-bill.service.ts new file mode 100644 index 0000000..e95547b --- /dev/null +++ b/apps/api/src/finance/vendor-bill.service.ts @@ -0,0 +1,480 @@ +import { + Injectable, + Logger, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { EntryPostingStatus, TaxNature } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { TaxService } from '../core/tax/tax.service'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { ThreeWayMatchService } from './three-way-match.service'; +import { + CreateVendorBillDto, + RecordVendorBillPaymentDto, +} from './dto/vendor-bill.dto'; + +type ResolvedTaxCode = Awaited>; + +type CalculatedVendorBillLine = { + materialId: string; + quantity: number; + unitPrice: number; + lineTotal: number; + subTotal: number; + taxAmount: number; + taxRate: number; + taxCodeId: string | null; + accountId?: string; + description?: string; +}; + +@Injectable() +export class VendorBillService { + private readonly logger = new Logger(VendorBillService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly eventEmitter: EventEmitter2, + private readonly taxService: TaxService, + private readonly threeWayMatchService: ThreeWayMatchService, + ) {} + + async create( + companyId: string, + dto: CreateVendorBillDto, + operatorId: string, + ) { + const partner = await this.prisma.partner.findFirst({ + where: { id: dto.partnerId, companyId, isActive: true }, + select: { id: true, name: true }, + }); + if (!partner) throw new NotFoundException('供应商不存在'); + + if (dto.receiptId) { + const receipt = await this.prisma.goodsReceipt.findFirst({ + where: { id: dto.receiptId, companyId }, + select: { id: true, partnerId: true }, + }); + if (!receipt) throw new NotFoundException('入库单不存在'); + if (receipt.partnerId !== dto.partnerId) { + throw new BadRequestException('入库单供应商与发票供应商不匹配'); + } + } + + if (!dto.lines || dto.lines.length === 0) { + throw new BadRequestException('采购发票至少需要一行明细'); + } + + const resolvedTaxCode = await this.taxService.resolveTaxCode( + companyId, + dto.taxCodeId, + { operatorId, entity: 'PurchaseInvoice', entityId: 'new' }, + ); + + const lineResults = await this.calculateLines( + companyId, + dto.lines, + resolvedTaxCode, + ); + const totals = this.calculateTotals(lineResults); + const invoiceNo = dto.invoiceNo ?? 'VB-' + Date.now(); + + return this.prisma.$transaction(async (tx) => { + const created = await tx.purchaseInvoice.create({ + data: { + invoiceNo, + partnerId: dto.partnerId, + receiptId: dto.receiptId ?? null, + amount: totals.totalAmount, + subTotal: totals.totalSubTotal, + taxAmount: totals.totalTaxAmount, + taxRate: resolvedTaxCode.rate, + taxCodeId: resolvedTaxCode.id ?? null, + taxNature: TaxNature.INPUT, + status: 'UNPAID', + postingStatus: EntryPostingStatus.DRAFT, + companyId, + dueDate: new Date(dto.dueDate), + notes: dto.notes ?? null, + lines: { + create: lineResults.map((line) => ({ + materialId: line.materialId, + quantity: line.quantity, + unitPrice: line.unitPrice, + lineTotal: line.lineTotal, + subTotal: line.subTotal, + taxAmount: line.taxAmount, + taxRate: line.taxRate, + taxCodeId: line.taxCodeId, + accountId: line.accountId ?? null, + description: line.description ?? null, + })), + }, + }, + include: { lines: true }, + }); + + await tx.auditLog.create({ + data: { + userId: operatorId, + action: 'CREATE_VENDOR_BILL', + entity: 'PurchaseInvoice', + entityId: created.id, + details: { + invoiceNo: created.invoiceNo, + partnerId: dto.partnerId, + receiptId: dto.receiptId ?? null, + amount: totals.totalAmount, + lineCount: lineResults.length, + }, + companyId, + }, + }); + + return created; + }); + } + + async createDraftFromReceipt( + companyId: string, + receiptId: string, + operatorId: string, + ) { + const receipt = await this.prisma.goodsReceipt.findFirst({ + where: { id: receiptId, companyId, status: 'CONFIRMED' }, + include: { lines: { include: { material: true } }, partner: true }, + }); + if (!receipt) throw new NotFoundException('入库单不存在或未确认'); + + const existing = await this.prisma.purchaseInvoice.findFirst({ + where: { receiptId, companyId }, + select: { id: true }, + }); + if (existing) { + this.logger.warn(`入库单 ${receipt.receiptNo} 已关联应付单,跳过`); + return existing; + } + + const resolvedTaxCode = await this.taxService.resolveTaxCode( + companyId, + null, + ); + const lineResults: CalculatedVendorBillLine[] = receipt.lines.map( + (line) => { + const unitPrice = Number(line.material?.unitPrice ?? 0); + const lineTotal = this.taxService.round2(line.quantity * unitPrice); + const breakdown = this.taxService.calcTaxBreakdown( + lineTotal, + resolvedTaxCode.rate, + resolvedTaxCode.isTaxInclusive, + TaxNature.INPUT, + ); + return { + materialId: line.materialId, + quantity: line.quantity, + unitPrice, + lineTotal, + subTotal: breakdown.subTotal, + taxAmount: breakdown.taxAmount, + taxRate: breakdown.taxRate, + taxCodeId: resolvedTaxCode.id ?? null, + }; + }, + ); + + const totals = this.calculateTotals(lineResults); + + const invoice = await this.prisma.$transaction(async (tx) => { + const created = await tx.purchaseInvoice.create({ + data: { + invoiceNo: 'VB-' + Date.now(), + partnerId: receipt.partnerId, + receiptId, + amount: totals.totalAmount, + subTotal: totals.totalSubTotal, + taxAmount: totals.totalTaxAmount, + taxRate: resolvedTaxCode.rate, + taxCodeId: resolvedTaxCode.id ?? null, + taxNature: TaxNature.INPUT, + status: 'DRAFT', + postingStatus: EntryPostingStatus.DRAFT, + companyId, + dueDate: new Date(Date.now() + 30 * 86400000), + notes: `自动生成自入库单 ${receipt.receiptNo}`, + lines: { create: lineResults }, + }, + include: { lines: true }, + }); + + await tx.auditLog.create({ + data: { + userId: operatorId, + action: 'AUTO_CREATE_VENDOR_BILL', + entity: 'PurchaseInvoice', + entityId: created.id, + details: { + invoiceNo: created.invoiceNo, + receiptId, + receiptNo: receipt.receiptNo, + amount: totals.totalAmount, + }, + companyId, + }, + }); + + return created; + }); + + this.logger.log( + `入库单 ${receipt.receiptNo} 自动生成应付草稿 ${invoice.invoiceNo}`, + ); + return invoice; + } + + async findAll(companyId: string, pagination: PaginationDto) { + const { page = 1, limit = 20 } = pagination; + const where = { companyId }; + const [data, total] = await Promise.all([ + this.prisma.purchaseInvoice.findMany({ + where, + include: { + partner: { select: { id: true, name: true, code: true } }, + lines: { + include: { + material: { select: { id: true, name: true, sku: true } }, + }, + }, + payments: true, + taxCode: true, + receipt: { select: { id: true, receiptNo: true } }, + }, + orderBy: { issuedDate: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.purchaseInvoice.count({ where }), + ]); + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } + + async findOne(companyId: string, id: string) { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id, companyId }, + include: { + partner: true, + lines: { include: { material: true, taxCode: true } }, + payments: true, + taxCode: true, + receipt: { include: { lines: true } }, + }, + }); + if (!invoice) throw new NotFoundException('采购发票不存在'); + return invoice; + } + + async confirm(companyId: string, id: string, operatorId: string) { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id, companyId }, + select: { id: true, status: true, invoiceNo: true }, + }); + if (!invoice) throw new NotFoundException('采购发票不存在'); + if (invoice.status !== 'DRAFT') + throw new BadRequestException('只有草稿状态的应付单才能确认'); + + return this.prisma.$transaction(async (tx) => { + const result = await tx.purchaseInvoice.update({ + where: { id }, + data: { status: 'UNPAID' }, + }); + await tx.auditLog.create({ + data: { + userId: operatorId, + action: 'CONFIRM_VENDOR_BILL', + entity: 'PurchaseInvoice', + entityId: id, + details: { invoiceNo: invoice.invoiceNo }, + companyId, + }, + }); + return result; + }); + } + + async recordPayment( + companyId: string, + invoiceId: string, + dto: RecordVendorBillPaymentDto, + ) { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id: invoiceId, companyId }, + include: { payments: true }, + }); + if (!invoice) throw new NotFoundException('采购发票不存在'); + if (invoice.status === 'DRAFT') + throw new BadRequestException('请先确认应付单再登记付款'); + + return this.prisma.$transaction(async (tx) => { + const payment = await tx.vendorBillPayment.create({ + data: { + invoiceId, + amount: dto.amount, + method: dto.method, + notes: dto.notes ?? null, + }, + }); + const totalPaid = + invoice.payments.reduce( + (sum, paymentItem) => sum + Number(paymentItem.amount), + 0, + ) + dto.amount; + let newStatus = invoice.status; + if (totalPaid >= Number(invoice.amount)) newStatus = 'PAID'; + else if (totalPaid > 0) newStatus = 'PARTIAL'; + await tx.purchaseInvoice.update({ + where: { id: invoiceId }, + data: { status: newStatus }, + }); + return payment; + }); + } + + async post(companyId: string, invoiceId: string, operatorId: string) { + const invoice = await this.prisma.purchaseInvoice.findFirst({ + where: { id: invoiceId, companyId }, + include: { + partner: { select: { id: true } }, + taxCode: { + include: { account: true, outputAccount: true, inputAccount: true }, + }, + }, + }); + if (!invoice) throw new NotFoundException('采购发票不存在'); + + await this.threeWayMatchService.validateAndPersist( + companyId, + invoiceId, + operatorId, + ); + + if (invoice.postingStatus === EntryPostingStatus.POSTED) { + return { + invoiceId: invoice.id, + invoiceNo: invoice.invoiceNo, + postingStatus: invoice.postingStatus, + message: '采购发票已过账,无需重复处理', + }; + } + + const amount = this.taxService.round2(Number(invoice.amount)); + if (amount <= 0) throw new BadRequestException('采购发票金额必须大于0'); + + const updated = await this.prisma.purchaseInvoice.update({ + where: { id: invoice.id }, + data: { postingStatus: EntryPostingStatus.POSTED }, + }); + + const taxCodeId = invoice.taxCodeId ?? null; + let taxAccountId: string | null = null; + if (taxCodeId) { + const resolved = await this.taxService.resolveTaxCode( + companyId, + taxCodeId, + ); + taxAccountId = this.taxService.getTaxAccountId(resolved); + } + + await this.prisma.auditLog.create({ + data: { + userId: operatorId, + action: 'POST_VENDOR_BILL', + entity: 'PurchaseInvoice', + entityId: invoice.id, + details: { + invoiceNo: invoice.invoiceNo, + taxCodeId, + taxRate: invoice.taxRate, + taxNature: invoice.taxNature, + taxAccountId, + amount, + }, + companyId, + }, + }); + + this.eventEmitter.emit('finance.vendor_bill.posted', { + companyId, + idempotencyKey: 'vendor_bill_posted:' + invoice.id, + invoiceId: invoice.id, + taxCodeId, + taxAccountId, + taxRate: invoice.taxRate ?? 0.13, + operatorId, + }); + + return updated; + } + + private async calculateLines( + companyId: string, + lines: Array<{ + materialId: string; + quantity: number; + unitPrice: number; + taxCodeId?: string; + accountId?: string; + description?: string; + }>, + defaultTaxCode: ResolvedTaxCode, + ): Promise { + const results: CalculatedVendorBillLine[] = []; + for (const line of lines) { + const material = await this.prisma.material.findFirst({ + where: { id: line.materialId, companyId }, + select: { id: true }, + }); + if (!material) + throw new NotFoundException(`物料不存在: ${line.materialId}`); + + const lineTaxCode = line.taxCodeId + ? await this.taxService.resolveTaxCode(companyId, line.taxCodeId) + : defaultTaxCode; + + const lineTotal = this.taxService.round2(line.quantity * line.unitPrice); + const breakdown = this.taxService.calcTaxBreakdown( + lineTotal, + lineTaxCode.rate, + lineTaxCode.isTaxInclusive, + TaxNature.INPUT, + ); + results.push({ + materialId: line.materialId, + quantity: line.quantity, + unitPrice: line.unitPrice, + lineTotal, + subTotal: breakdown.subTotal, + taxAmount: breakdown.taxAmount, + taxRate: breakdown.taxRate, + taxCodeId: lineTaxCode.id ?? null, + accountId: line.accountId, + description: line.description, + }); + } + return results; + } + + private calculateTotals(lines: CalculatedVendorBillLine[]) { + return { + totalAmount: this.taxService.round2( + lines.reduce((sum, line) => sum + line.lineTotal, 0), + ), + totalSubTotal: this.taxService.round2( + lines.reduce((sum, line) => sum + line.subTotal, 0), + ), + totalTaxAmount: this.taxService.round2( + lines.reduce((sum, line) => sum + line.taxAmount, 0), + ), + }; + } +} diff --git a/apps/api/src/goods-receipts/dto/create-goods-receipt.dto.ts b/apps/api/src/goods-receipts/dto/create-goods-receipt.dto.ts new file mode 100644 index 0000000..8808d12 --- /dev/null +++ b/apps/api/src/goods-receipts/dto/create-goods-receipt.dto.ts @@ -0,0 +1,98 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsString, + IsNotEmpty, + IsNumber, + Min, + IsArray, + ValidateNested, + IsOptional, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +// ---- 收货单明细行 DTO ---- + +export class GoodsReceiptLineDto { + @ApiProperty({ description: '采购单行ID (PurchaseOrderLine.id)' }) + @IsString() + @IsNotEmpty() + orderLineId!: string; + + @ApiProperty({ description: '物料ID' }) + @IsString() + @IsNotEmpty() + materialId!: string; + + @ApiProperty({ description: '产品ID' }) + @IsString() + @IsNotEmpty() + productId!: string; + + @ApiProperty({ description: '目标库位ID' }) + @IsString() + @IsNotEmpty() + locationId!: string; + + @ApiProperty({ description: '收货数量' }) + @IsNumber() + @Min(0.01) + quantity!: number; + + @ApiPropertyOptional({ description: '批次号' }) + @IsOptional() + @IsString() + batchNo?: string; +} + +// ---- 创建收货单 DTO ---- + +export class CreateGoodsReceiptDto { + @ApiProperty({ description: '关联采购单ID' }) + @IsString() + @IsNotEmpty() + purchaseOrderId!: string; + + @ApiProperty({ description: '供应商ID (Partner.id)' }) + @IsString() + @IsNotEmpty() + partnerId!: string; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + notes?: string; + + @ApiProperty({ + description: '收货单明细行', + type: [GoodsReceiptLineDto], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => GoodsReceiptLineDto) + lines!: GoodsReceiptLineDto[]; +} + +// ---- 确认收货 DTO ---- + +export class ConfirmGoodsReceiptDto { + @ApiPropertyOptional({ description: '确认备注' }) + @IsOptional() + @IsString() + note?: string; +} + +// ---- 冲销收货单 DTO ---- + +export class ReverseGoodsReceiptDto { + @ApiPropertyOptional({ description: '冲销备注' }) + @IsOptional() + @IsString() + note?: string; + + @ApiPropertyOptional({ + description: '冲销出库来源库位ID(默认取原收货入库目标库位)', + }) + @IsOptional() + @IsString() + sourceLocationId?: string; +} diff --git a/apps/api/src/goods-receipts/goods-receipts.controller.ts b/apps/api/src/goods-receipts/goods-receipts.controller.ts new file mode 100644 index 0000000..6d88d98 --- /dev/null +++ b/apps/api/src/goods-receipts/goods-receipts.controller.ts @@ -0,0 +1,86 @@ +import { + Controller, + Get, + Post, + Body, + Param, + UseGuards, + Query, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { GoodsReceiptsService } from './goods-receipts.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; +import { CurrentCompany } from '../core/decorators/current-company.decorator'; +import { CurrentUser } from '../core/decorators/current-user.decorator'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { + CreateGoodsReceiptDto, + ConfirmGoodsReceiptDto, + ReverseGoodsReceiptDto, +} from './dto/create-goods-receipt.dto'; +import type { JwtUserPayload } from '../core/http/request.types'; + +@ApiTags('收货单管理 (Goods Receipts)') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, TenantGuard) +@Controller('goods-receipts') +export class GoodsReceiptsController { + constructor(private readonly service: GoodsReceiptsService) {} + + @Post() + @ApiOperation({ summary: '创建收货单(关联采购单)' }) + async create( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Body() dto: CreateGoodsReceiptDto, + ) { + return this.service.create(companyId, user.id, dto); + } + + @Get() + @ApiOperation({ summary: '获取收货单列表(支持分页)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + async findAll( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + @Query('status') status?: string, + ) { + return this.service.findAll(companyId, pagination, status); + } + + @Get(':id') + @ApiOperation({ summary: '获取收货单详情' }) + async findOne(@Param('id') id: string, @CurrentCompany() companyId: string) { + return this.service.findOne(id, companyId); + } + + @Post(':id/confirm') + @ApiOperation({ summary: '确认收货 → 触发库存入库(幂等)' }) + async confirm( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') id: string, + @Body() dto: ConfirmGoodsReceiptDto, + ) { + return this.service.confirm(companyId, id, dto, user.id); + } + + @Post(':id/reverse') + @ApiOperation({ summary: '冲销收货单 → 生成反向库存流水(幂等)' }) + async reverse( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Param('id') id: string, + @Body() dto: ReverseGoodsReceiptDto, + ) { + return this.service.reverse(companyId, id, dto, user.id); + } +} diff --git a/apps/api/src/goods-receipts/goods-receipts.module.ts b/apps/api/src/goods-receipts/goods-receipts.module.ts new file mode 100644 index 0000000..5a14957 --- /dev/null +++ b/apps/api/src/goods-receipts/goods-receipts.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { GoodsReceiptsController } from './goods-receipts.controller'; +import { GoodsReceiptsService } from './goods-receipts.service'; +import { InventoryModule } from '../inventory/inventory.module'; + +@Module({ + imports: [InventoryModule], + controllers: [GoodsReceiptsController], + providers: [GoodsReceiptsService], + exports: [GoodsReceiptsService], +}) +export class GoodsReceiptsModule {} diff --git a/apps/api/src/goods-receipts/goods-receipts.service.spec.ts b/apps/api/src/goods-receipts/goods-receipts.service.spec.ts new file mode 100644 index 0000000..a3b3de4 --- /dev/null +++ b/apps/api/src/goods-receipts/goods-receipts.service.spec.ts @@ -0,0 +1,173 @@ +import { BadRequestException } from '@nestjs/common'; +import { GoodsReceiptsService } from './goods-receipts.service'; +import { PurchaseOrderStatus } from '@prisma/client'; + +type MockPrisma = { + purchaseOrder: { findFirst: jest.Mock }; + goodsReceipt: { create: jest.Mock; findFirst: jest.Mock }; + auditLog: { create: jest.Mock }; + inventoryTransaction: { count: jest.Mock }; + $transaction: jest.Mock; +}; + +type MockTx = { + purchaseOrderLine: { update: jest.Mock; findMany: jest.Mock }; + purchaseOrder: { update: jest.Mock }; + goodsReceipt: { update: jest.Mock }; +}; + +describe('GoodsReceiptsService', () => { + const prisma: MockPrisma = { + purchaseOrder: { findFirst: jest.fn() }, + goodsReceipt: { create: jest.fn(), findFirst: jest.fn() }, + auditLog: { create: jest.fn() }, + inventoryTransaction: { count: jest.fn() }, + $transaction: jest.fn(), + }; + const tx: MockTx = { + purchaseOrderLine: { update: jest.fn(), findMany: jest.fn() }, + purchaseOrder: { update: jest.fn() }, + goodsReceipt: { update: jest.fn() }, + }; + const inventoryService = { createStockMove: jest.fn() }; + let service: GoodsReceiptsService; + + beforeEach(() => { + jest.clearAllMocks(); + prisma.$transaction.mockImplementation( + (callback: (trx: MockTx) => unknown) => callback(tx), + ); + service = new GoodsReceiptsService( + prisma as unknown as ConstructorParameters< + typeof GoodsReceiptsService + >[0], + inventoryService as unknown as ConstructorParameters< + typeof GoodsReceiptsService + >[1], + ); + }); + + it('rejects receipt creation when an order line cannot be matched uniquely later', async () => { + prisma.purchaseOrder.findFirst.mockResolvedValue({ + id: 'po1', + partnerId: 'supplier1', + status: PurchaseOrderStatus.APPROVED, + lines: [ + { + id: 'line1', + materialId: 'mat1', + productId: 'prod1', + quantity: 5, + receivedQuantity: 0, + }, + { + id: 'line2', + materialId: 'mat1', + productId: 'prod1', + quantity: 3, + receivedQuantity: 0, + }, + ], + }); + + await expect( + service.create('company1', 'user1', { + purchaseOrderId: 'po1', + partnerId: 'supplier1', + lines: [ + { + orderLineId: 'line1', + materialId: 'mat1', + productId: 'prod1', + locationId: 'loc1', + quantity: 2, + }, + ], + }), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.goodsReceipt.create).not.toHaveBeenCalled(); + }); + + it('skips confirming an already confirmed receipt', async () => { + prisma.goodsReceipt.findFirst.mockResolvedValue({ + id: 'gr1', + receiptNo: 'GR-001', + status: 'CONFIRMED', + lines: [], + order: null, + }); + + const result = await service.confirm('company1', 'gr1', {}, 'user1'); + + expect(result.status).toBe('CONFIRMED'); + expect(result.message).toContain('跳过重复处理'); + expect(prisma.$transaction).not.toHaveBeenCalled(); + expect(inventoryService.createStockMove).not.toHaveBeenCalled(); + }); + + it('increments the uniquely matching purchase order line when confirming', async () => { + prisma.goodsReceipt.findFirst.mockResolvedValue({ + id: 'gr1', + receiptNo: 'GR-001', + status: 'DRAFT', + lines: [ + { + id: 'grLine1', + materialId: 'mat1', + productId: 'prod1', + locationId: 'loc1', + quantity: 2, + batchNo: null, + }, + ], + order: { + id: 'po1', + status: PurchaseOrderStatus.APPROVED, + lines: [ + { + id: 'poLine1', + materialId: 'mat1', + productId: 'prod1', + quantity: 5, + receivedQuantity: 0, + }, + ], + }, + }); + inventoryService.createStockMove.mockResolvedValue({ id: 'txn1' }); + tx.purchaseOrderLine.findMany.mockResolvedValue([ + { id: 'poLine1', quantity: 5, receivedQuantity: 2 }, + ]); + + const result = await service.confirm('company1', 'gr1', {}, 'user1'); + + expect(result.status).toBe('CONFIRMED'); + expect(tx.purchaseOrderLine.update).toHaveBeenCalledWith({ + where: { id: 'poLine1' }, + data: { receivedQuantity: { increment: 2 } }, + }); + expect(tx.purchaseOrder.update).toHaveBeenCalledWith({ + where: { id: 'po1' }, + data: { status: PurchaseOrderStatus.PARTIALLY_RECEIVED }, + }); + }); + + it('skips reversing when reverse stock moves already exist', async () => { + prisma.goodsReceipt.findFirst.mockResolvedValue({ + id: 'gr1', + receiptNo: 'GR-001', + status: 'CONFIRMED', + lines: [{ id: 'grLine1', materialId: 'mat1', productId: 'prod1' }], + orderId: 'po1', + order: { lines: [] }, + }); + prisma.inventoryTransaction.count.mockResolvedValue(1); + + const result = await service.reverse('company1', 'gr1', {}, 'user1'); + + expect(result.message).toContain('跳过重复处理'); + expect(result.reversedLines).toEqual([]); + expect(prisma.$transaction).not.toHaveBeenCalled(); + expect(inventoryService.createStockMove).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/goods-receipts/goods-receipts.service.ts b/apps/api/src/goods-receipts/goods-receipts.service.ts new file mode 100644 index 0000000..f6610f8 --- /dev/null +++ b/apps/api/src/goods-receipts/goods-receipts.service.ts @@ -0,0 +1,497 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { + GoodsReceiptStatus, + Prisma, + PurchaseOrderStatus, +} from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { InventoryService } from '../inventory/inventory.service'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { + CreateGoodsReceiptDto, + ConfirmGoodsReceiptDto, + ReverseGoodsReceiptDto, +} from './dto/create-goods-receipt.dto'; + +@Injectable() +export class GoodsReceiptsService { + private readonly logger = new Logger(GoodsReceiptsService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly inventoryService: InventoryService, + ) {} + + private generateReceiptNo(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const seq = Math.floor(1000 + Math.random() * 9000); + return `GR-${year}${month}-${seq}`; + } + + private auditLog( + userId: string, + action: string, + entityId: string, + details: Prisma.InputJsonValue, + companyId: string, + ) { + return this.prisma.auditLog.create({ + data: { + userId, + action, + entity: 'goods_receipt', + entityId, + details, + companyId, + }, + }); + } + + private receiptLineKey(line: { + materialId: string | null; + productId: string | null; + }) { + return `${line.materialId ?? ''}:${line.productId ?? ''}`; + } + + private findUniqueOrderLineForReceiptLine( + receiptLine: { id?: string; materialId: string; productId: string }, + orderLines: Array<{ + id: string; + materialId: string | null; + productId: string | null; + receivedQuantity: number; + }>, + ) { + const matches = orderLines.filter( + (line) => + line.materialId === receiptLine.materialId && + line.productId === receiptLine.productId, + ); + if (matches.length !== 1) { + throw new BadRequestException( + `收货单行 ${receiptLine.id ?? this.receiptLineKey(receiptLine)} 无法唯一匹配采购单行`, + ); + } + return matches[0]; + } + + // ======================================= + // 创建收货单 + // ======================================= + async create(companyId: string, userId: string, dto: CreateGoodsReceiptDto) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: dto.purchaseOrderId, companyId }, + include: { lines: true }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + if (po.status === 'CANCELLED') + throw new BadRequestException('已取消的采购单不能创建收货单'); + if (!['APPROVED', 'PARTIALLY_RECEIVED'].includes(po.status)) { + throw new BadRequestException( + '只有已审批或部分收货的采购单才能创建收货单', + ); + } + if (po.partnerId !== dto.partnerId) { + throw new BadRequestException('供应商与采购单不匹配'); + } + + const poLineMap = new Map(po.lines.map((l) => [l.id, l])); + const seenOrderLineIds = new Set(); + for (const line of dto.lines) { + if (seenOrderLineIds.has(line.orderLineId)) { + throw new BadRequestException(`采购单行 ${line.orderLineId} 重复收货`); + } + seenOrderLineIds.add(line.orderLineId); + + const poLine = poLineMap.get(line.orderLineId); + if (!poLine) + throw new BadRequestException(`采购单行 ${line.orderLineId} 不存在`); + if ( + poLine.materialId !== line.materialId || + poLine.productId !== line.productId + ) { + throw new BadRequestException( + `收货单行与采购单行 ${line.orderLineId} 不匹配`, + ); + } + const sameItemLineCount = po.lines.filter( + (candidate) => + candidate.materialId === line.materialId && + candidate.productId === line.productId, + ).length; + if (sameItemLineCount !== 1) { + throw new BadRequestException( + `采购单行 ${line.orderLineId} 的物料/产品组合不唯一,无法安全创建收货单`, + ); + } + const remaining = poLine.quantity - poLine.receivedQuantity; + if (line.quantity > remaining) { + throw new BadRequestException( + `物料 ${line.materialId} 收货数量(${line.quantity})超过可收货数量(${remaining})`, + ); + } + } + + const receiptNo = this.generateReceiptNo(); + const receipt = await this.prisma.goodsReceipt.create({ + data: { + receiptNo, + orderId: dto.purchaseOrderId, + partnerId: dto.partnerId, + status: 'DRAFT', + notes: dto.notes, + companyId, + lines: { + create: dto.lines.map((line) => ({ + productId: line.productId, + materialId: line.materialId, + locationId: line.locationId, + quantity: line.quantity, + batchNo: line.batchNo ?? null, + })), + }, + }, + include: { lines: true }, + }); + + await this.auditLog( + userId, + 'CREATE_GOODS_RECEIPT', + receipt.id, + { + receiptNo, + purchaseOrderId: dto.purchaseOrderId, + lineCount: dto.lines.length, + }, + companyId, + ); + + this.logger.log(`收货单 ${receiptNo} 创建成功`); + return receipt; + } + + // ======================================= + // 确认收货 → 触发库存入库 + // ======================================= + async confirm( + companyId: string, + receiptId: string, + dto: ConfirmGoodsReceiptDto, + operatorId: string, + ) { + const receipt = await this.prisma.goodsReceipt.findFirst({ + where: { id: receiptId, companyId }, + include: { + lines: true, + order: { include: { lines: true } }, + }, + }); + + if (!receipt) throw new NotFoundException('收货单不存在或无权操作'); + if (receipt.status === 'CONFIRMED') { + return { + receiptId: receipt.id, + receiptNo: receipt.receiptNo, + status: receipt.status, + message: '收货单已确认,跳过重复处理', + }; + } + if (receipt.status === 'CANCELLED') + throw new BadRequestException('已取消的收货单不能确认'); + if (!receipt.lines.length) + throw new BadRequestException('收货单无明细行,无法确认'); + + const referenceNo = `GR-${receipt.receiptNo}`; + const confirmedLines: Array<{ + lineId: string; + materialId: string; + quantity: number; + transactionId: string; + }> = []; + + await this.prisma.$transaction(async (tx) => { + for (const line of receipt.lines) { + const transaction = await this.inventoryService.createStockMove( + companyId, + { + materialId: line.materialId, + destLocationId: line.locationId, + quantity: line.quantity, + batchNo: line.batchNo ?? undefined, + referenceNo, + documentType: 'GOODS_RECEIPT', + documentId: receipt.id, + note: dto.note ?? `收货单确认入库:${receipt.receiptNo}`, + }, + operatorId, + ); + confirmedLines.push({ + lineId: line.id, + materialId: line.materialId, + quantity: line.quantity, + transactionId: transaction.id, + }); + } + + // 更新采购单行已收货数量 + if (receipt.order) { + for (const line of receipt.lines) { + const matchedPoLine = this.findUniqueOrderLineForReceiptLine( + line, + receipt.order.lines, + ); + await tx.purchaseOrderLine.update({ + where: { id: matchedPoLine.id }, + data: { receivedQuantity: { increment: line.quantity } }, + }); + } + + const updatedPoLines = await tx.purchaseOrderLine.findMany({ + where: { orderId: receipt.order.id }, + }); + const allReceived = updatedPoLines.every( + (l) => l.receivedQuantity >= l.quantity, + ); + const anyReceived = updatedPoLines.some((l) => l.receivedQuantity > 0); + let newStatus = receipt.order.status; + if (allReceived) newStatus = PurchaseOrderStatus.RECEIVED; + else if (anyReceived) + newStatus = PurchaseOrderStatus.PARTIALLY_RECEIVED; + if (newStatus !== receipt.order.status) { + await tx.purchaseOrder.update({ + where: { id: receipt.order.id }, + data: { status: newStatus }, + }); + } + } + + await tx.goodsReceipt.update({ + where: { id: receipt.id }, + data: { status: 'CONFIRMED' }, + }); + }); + + await this.auditLog( + operatorId, + 'CONFIRM_GOODS_RECEIPT', + receipt.id, + { + receiptNo: receipt.receiptNo, + confirmedLineCount: confirmedLines.length, + }, + companyId, + ); + + this.logger.log( + `收货单 ${receipt.receiptNo} 已确认,${confirmedLines.length} 行已入库`, + ); + return { + receiptId: receipt.id, + receiptNo: receipt.receiptNo, + status: 'CONFIRMED', + confirmedLines, + message: `收货单已确认完成,${confirmedLines.length} 行已入库`, + }; + } + + // ======================================= + // 冲销收货单 → 生成反向库存流水 + // ======================================= + async reverse( + companyId: string, + receiptId: string, + dto: ReverseGoodsReceiptDto, + operatorId: string, + ) { + const receipt = await this.prisma.goodsReceipt.findFirst({ + where: { id: receiptId, companyId }, + include: { lines: true, order: { include: { lines: true } } }, + }); + + if (!receipt) throw new NotFoundException('收货单不存在或无权操作'); + if (receipt.status !== 'CONFIRMED') + throw new BadRequestException('只有已确认的收货单才能冲销'); + + const referenceNo = `GR-REV-${receipt.receiptNo}`; + const reverseNote = dto.note ?? `收货单冲销:${receipt.receiptNo}`; + + const existedReverse = await this.prisma.inventoryTransaction.count({ + where: { companyId, referenceNo }, + }); + if (existedReverse > 0) { + return { + receiptId: receipt.id, + receiptNo: receipt.receiptNo, + reversedLines: [], + message: '收货单已冲销,跳过重复处理', + }; + } + + const reversedLines: Array<{ + lineId: string; + materialId: string; + quantity: number; + transactionId: string; + }> = []; + + await this.prisma.$transaction(async (tx) => { + for (const line of receipt.lines) { + const transaction = await this.inventoryService.createStockMove( + companyId, + { + materialId: line.materialId, + sourceLocationId: + dto.sourceLocationId ?? line.locationId ?? undefined, + quantity: line.quantity, + batchNo: line.batchNo ?? undefined, + referenceNo, + documentType: 'GOODS_RECEIPT_REVERSE', + documentId: receipt.id, + note: `${reverseNote} (行${line.id})`, + }, + operatorId, + ); + reversedLines.push({ + lineId: line.id, + materialId: line.materialId, + quantity: line.quantity, + transactionId: transaction.id, + }); + } + + // 回退采购单行已收货数量 + if (receipt.orderId && receipt.order) { + for (const line of receipt.lines) { + const matchedPoLine = this.findUniqueOrderLineForReceiptLine( + line, + receipt.order.lines, + ); + await tx.purchaseOrderLine.update({ + where: { id: matchedPoLine.id }, + data: { + receivedQuantity: { + decrement: Math.min( + line.quantity, + matchedPoLine.receivedQuantity, + ), + }, + }, + }); + } + const updatedPoLines = await tx.purchaseOrderLine.findMany({ + where: { orderId: receipt.orderId }, + }); + const allReceived = updatedPoLines.every( + (l) => l.receivedQuantity >= l.quantity, + ); + const anyReceived = updatedPoLines.some((l) => l.receivedQuantity > 0); + let newStatus: PurchaseOrderStatus = PurchaseOrderStatus.APPROVED; + if (allReceived) newStatus = PurchaseOrderStatus.RECEIVED; + else if (anyReceived) + newStatus = PurchaseOrderStatus.PARTIALLY_RECEIVED; + await tx.purchaseOrder.update({ + where: { id: receipt.orderId }, + data: { status: newStatus }, + }); + } + + await tx.goodsReceipt.update({ + where: { id: receipt.id }, + data: { status: 'CANCELLED' }, + }); + }); + + await this.auditLog( + operatorId, + 'REVERSE_GOODS_RECEIPT', + receipt.id, + { + receiptNo: receipt.receiptNo, + reversedLineCount: reversedLines.length, + }, + companyId, + ); + + this.logger.log( + `收货单 ${receipt.receiptNo} 已冲销,${reversedLines.length} 行已执行反向出库`, + ); + return { + receiptId: receipt.id, + receiptNo: receipt.receiptNo, + status: 'CANCELLED', + reversedLines, + message: `收货单已冲销完成,${reversedLines.length} 行已执行反向出库`, + }; + } + + // ======================================= + // 查询收货单列表 + // ======================================= + async findAll(companyId: string, pagination: PaginationDto, status?: string) { + const { page = 1, limit = 20 } = pagination; + const where: Prisma.GoodsReceiptWhereInput = { companyId }; + if (status && status in GoodsReceiptStatus) { + where.status = status as GoodsReceiptStatus; + } + + const [data, total] = await Promise.all([ + this.prisma.goodsReceipt.findMany({ + where, + include: { + partner: { select: { id: true, name: true, code: true } }, + order: { select: { id: true, orderNo: true } }, + lines: { + include: { + material: { select: { id: true, sku: true, name: true } }, + location: { select: { id: true, name: true } }, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.goodsReceipt.count({ where }), + ]); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } + + // ======================================= + // 查询收货单详情 + // ======================================= + async findOne(receiptId: string, companyId: string) { + const receipt = await this.prisma.goodsReceipt.findFirst({ + where: { id: receiptId, companyId }, + include: { + partner: true, + order: { + include: { + lines: { + include: { + material: { select: { id: true, sku: true, name: true } }, + }, + }, + }, + }, + lines: { + include: { + material: true, + product: true, + location: { include: { warehouse: true } }, + }, + }, + }, + }); + if (!receipt) throw new NotFoundException('收货单不存在或无权操作'); + return receipt; + } +} diff --git a/apps/api/src/inventory/dto/inventory.dto.ts b/apps/api/src/inventory/dto/inventory.dto.ts index 6dfb3e3..bf83667 100644 --- a/apps/api/src/inventory/dto/inventory.dto.ts +++ b/apps/api/src/inventory/dto/inventory.dto.ts @@ -181,3 +181,26 @@ export class ReversePurchaseInboundDto { @IsString() note?: string; } + +export class CreatePickingDto { + @ApiPropertyOptional({ description: '来源库位ID(整单出库时统一指定)' }) + @IsOptional() + @IsString() + sourceLocationId?: string; + + @ApiPropertyOptional({ description: '目标库位ID(整单入库时统一指定)' }) + @IsOptional() + @IsString() + destLocationId?: string; + + @ApiPropertyOptional({ description: '计划执行日期' }) + @IsOptional() + scheduledDate?: string; +} + +export class ConfirmPickingDto { + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + note?: string; +} diff --git a/apps/api/src/inventory/inventory.controller.ts b/apps/api/src/inventory/inventory.controller.ts index d4c6252..5677134 100644 --- a/apps/api/src/inventory/inventory.controller.ts +++ b/apps/api/src/inventory/inventory.controller.ts @@ -19,7 +19,9 @@ import { TenantGuard } from '../core/guards/tenant.guard'; import { CurrentCompany } from '../core/decorators/current-company.decorator'; import { CurrentUser } from '../core/decorators/current-user.decorator'; import { + ConfirmPickingDto, CreateInboundDto, + CreatePickingDto, CreateStockMoveDto, PurchaseInboundPostingDto, ReversePurchaseInboundDto, @@ -87,6 +89,64 @@ export class InventoryController { return this.inventoryService.getRealtimeLedger(companyId); } + // ---- Stock Picking Endpoints ---- + + @Get('pickings') + @ApiOperation({ summary: '获取拣货单列表(支持分页)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + async getPickings( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + @Query('status') status?: string, + ) { + return this.inventoryService.getPickings(companyId, pagination, status); + } + + @Get('pickings/:pickingId') + @ApiOperation({ summary: '获取拣货单详情' }) + async getPickingById( + @Param('pickingId') pickingId: string, + @CurrentCompany() companyId: string, + ) { + return this.inventoryService.getPickingById(pickingId, companyId); + } + + @Post('pickings/sale-order/:orderId') + @ApiOperation({ summary: '为销售订单创建出库拣货单(DRAFT)' }) + async createSaleOrderPicking( + @CurrentCompany() companyId: string, + @CurrentUser() user: CurrentUserPayload, + @Param('orderId') orderId: string, + @Body() payload: CreatePickingDto, + ) { + return this.inventoryService.createSaleOrderPicking( + companyId, + orderId, + payload, + user.id, + ); + } + + @Post('pickings/:pickingId/confirm') + @ApiOperation({ summary: '确认拣货单,执行库存扣减(幂等)' }) + async confirmStockPicking( + @CurrentCompany() companyId: string, + @CurrentUser() user: CurrentUserPayload, + @Param('pickingId') pickingId: string, + @Body() payload: ConfirmPickingDto, + ) { + return this.inventoryService.confirmStockPicking( + companyId, + pickingId, + payload, + user.id, + ); + } + + // ---- Legacy / Convenience Endpoints ---- + @Post('inbound') @ApiOperation({ summary: '新建入库单' }) async createInbound( @@ -134,7 +194,9 @@ export class InventoryController { } @Post('posting/sale-order/:orderId/ship') - @ApiOperation({ summary: '按销售订单自动过账并出库' }) + @ApiOperation({ + summary: '按销售订单自动过账并出库(便捷方法,内部走 Picking 流程)', + }) async postSaleOrderShipment( @CurrentCompany() companyId: string, @CurrentUser() user: CurrentUserPayload, diff --git a/apps/api/src/inventory/inventory.service.spec.ts b/apps/api/src/inventory/inventory.service.spec.ts index 525a9d1..343816b 100644 --- a/apps/api/src/inventory/inventory.service.spec.ts +++ b/apps/api/src/inventory/inventory.service.spec.ts @@ -6,20 +6,19 @@ import { import { InventoryService } from './inventory.service'; type MockPrisma = { - material: { - findFirst: jest.Mock; - }; - stockLocation: { - findFirst: jest.Mock; - }; - order: { + material: { findFirst: jest.Mock }; + stockLocation: { findFirst: jest.Mock }; + order: { findFirst: jest.Mock; update: jest.Mock }; + product: { findFirst: jest.Mock }; + inventoryTransaction: { count: jest.Mock; findMany: jest.Mock }; + stockPicking: { findFirst: jest.Mock; + create: jest.Mock; update: jest.Mock; - }; - inventoryTransaction: { - count: jest.Mock; findMany: jest.Mock; + count: jest.Mock; }; + stockMove: { update: jest.Mock; updateMany: jest.Mock }; $transaction: jest.Mock; }; @@ -29,27 +28,27 @@ type MockTx = { findFirst: jest.Mock; upsert: jest.Mock; }; - inventoryTransaction: { - create: jest.Mock; - }; + inventoryTransaction: { create: jest.Mock }; + stockMove: { update: jest.Mock; updateMany: jest.Mock }; + stockPicking: { update: jest.Mock }; + order: { update: jest.Mock }; }; describe('InventoryService', () => { const prisma: MockPrisma = { - material: { - findFirst: jest.fn(), - }, - stockLocation: { - findFirst: jest.fn(), - }, - order: { + material: { findFirst: jest.fn() }, + stockLocation: { findFirst: jest.fn() }, + order: { findFirst: jest.fn(), update: jest.fn() }, + product: { findFirst: jest.fn() }, + inventoryTransaction: { count: jest.fn(), findMany: jest.fn() }, + stockPicking: { findFirst: jest.fn(), + create: jest.fn(), update: jest.fn(), - }, - inventoryTransaction: { - count: jest.fn(), findMany: jest.fn(), + count: jest.fn(), }, + stockMove: { update: jest.fn(), updateMany: jest.fn() }, $transaction: jest.fn(), }; @@ -59,20 +58,15 @@ describe('InventoryService', () => { findFirst: jest.fn(), upsert: jest.fn(), }, - inventoryTransaction: { - create: jest.fn(), - }, - }; - - const eventEmitter = { - emit: jest.fn(), + inventoryTransaction: { create: jest.fn() }, + stockMove: { update: jest.fn(), updateMany: jest.fn() }, + stockPicking: { update: jest.fn() }, + order: { update: jest.fn() }, }; + const eventEmitter = { emit: jest.fn() }; let service: InventoryService; - - const kyselyService = { - withTenant: jest.fn(), - }; + const kyselyService = { withTenant: jest.fn() }; beforeEach(() => { jest.clearAllMocks(); @@ -92,7 +86,6 @@ describe('InventoryService', () => { it('throws when sale order is missing on reverse posting', async () => { prisma.order.findFirst.mockResolvedValue(null); - await expect( service.reverseSaleOrderShipment('c1', 'o1', {}, 'u1'), ).rejects.toBeInstanceOf(NotFoundException); @@ -101,46 +94,19 @@ describe('InventoryService', () => { it('skips reverse when reverse moves already exist', async () => { prisma.order.findFirst.mockResolvedValue({ id: 'o1', orderNo: 'ORD-001' }); prisma.inventoryTransaction.count.mockResolvedValue(1); - const result = await service.reverseSaleOrderShipment('c1', 'o1', {}, 'u1'); - expect(result.message).toContain('已存在'); - expect(prisma.inventoryTransaction.findMany).not.toHaveBeenCalled(); }); it('throws when no shipped outbound moves found', async () => { prisma.order.findFirst.mockResolvedValue({ id: 'o1', orderNo: 'ORD-001' }); prisma.inventoryTransaction.count.mockResolvedValue(0); prisma.inventoryTransaction.findMany.mockResolvedValue([]); - await expect( service.reverseSaleOrderShipment('c1', 'o1', {}, 'u1'), ).rejects.toBeInstanceOf(BadRequestException); }); - it('skips purchase reverse when reverse moves already exist', async () => { - prisma.inventoryTransaction.count.mockResolvedValue(1); - - const result = await service.reversePurchaseInbound( - 'c1', - 'PO-001', - {}, - 'u1', - ); - - expect(result.message).toContain('已存在'); - expect(prisma.inventoryTransaction.findMany).not.toHaveBeenCalled(); - }); - - it('throws when purchase inbound moves are missing', async () => { - prisma.inventoryTransaction.count.mockResolvedValue(0); - prisma.inventoryTransaction.findMany.mockResolvedValue([]); - - await expect( - service.reversePurchaseInbound('c1', 'PO-001', {}, 'u1'), - ).rejects.toBeInstanceOf(BadRequestException); - }); - it('throws conflict when outbound conditional deduction fails', async () => { prisma.stockLocation.findFirst.mockResolvedValue({ id: 'loc-source', @@ -149,7 +115,6 @@ describe('InventoryService', () => { }); prisma.material.findFirst.mockResolvedValue({ id: 'm1', unitPrice: 10 }); tx.stockQuant.updateMany.mockResolvedValue({ count: 0 }); - await expect( service.createStockMove( 'c1', @@ -162,62 +127,290 @@ describe('InventoryService', () => { 'u1', ), ).rejects.toBeInstanceOf(ConflictException); + }); + describe('createSaleOrderPicking', () => { + it('throws when order not found', async () => { + prisma.stockLocation.findFirst.mockResolvedValue({ + id: 'loc-source', + name: 'L1', + warehouseId: 'w1', + }); + prisma.order.findFirst.mockResolvedValue(null); + await expect( + service.createSaleOrderPicking( + 'c1', + 'o1', + { sourceLocationId: 'loc-source' }, + 'u1', + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); - expect(tx.inventoryTransaction.create).not.toHaveBeenCalled(); + it('throws when order has no items', async () => { + prisma.stockLocation.findFirst.mockResolvedValue({ + id: 'loc-source', + name: 'L1', + warehouseId: 'w1', + }); + prisma.order.findFirst.mockResolvedValue({ + id: 'o1', + orderNo: 'ORD-001', + items: [], + }); + await expect( + service.createSaleOrderPicking( + 'c1', + 'o1', + { sourceLocationId: 'loc-source' }, + 'u1', + ), + ).rejects.toBeInstanceOf(BadRequestException); + }); + + it('requires a source location for sale order picking', async () => { + await expect( + service.createSaleOrderPicking('c1', 'o1', {}, 'u1'), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.order.findFirst).not.toHaveBeenCalled(); + }); + + it('returns existing picking when one already exists', async () => { + prisma.stockLocation.findFirst.mockResolvedValue({ + id: 'loc-source', + name: 'L1', + warehouseId: 'w1', + }); + prisma.order.findFirst.mockResolvedValue({ + id: 'o1', + orderNo: 'ORD-001', + items: [{ productId: 'p1', quantity: 5 }], + }); + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DRAFT', + }); + const result = await service.createSaleOrderPicking( + 'c1', + 'o1', + { sourceLocationId: 'loc-source' }, + 'u1', + ); + expect(result.pickingId).toBe('pk1'); + expect(result.message).toContain('已存在'); + expect(prisma.stockPicking.create).not.toHaveBeenCalled(); + }); + + it('creates picking with moves from order items', async () => { + prisma.stockLocation.findFirst.mockResolvedValue({ + id: 'loc-source', + name: 'L1', + warehouseId: 'w1', + }); + prisma.order.findFirst.mockResolvedValue({ + id: 'o1', + orderNo: 'ORD-001', + items: [ + { productId: 'p1', quantity: 3 }, + { productId: 'p2', quantity: 2 }, + ], + }); + prisma.stockPicking.findFirst.mockResolvedValue(null); + prisma.product.findFirst + .mockResolvedValueOnce({ id: 'p1', materialId: 'm1', name: 'Product1' }) + .mockResolvedValueOnce({ + id: 'p2', + materialId: 'm2', + name: 'Product2', + }); + prisma.stockPicking.create.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DRAFT', + moves: [ + { id: 'mv1', lineNo: 1, materialId: 'm1', quantity: 3 }, + { id: 'mv2', lineNo: 2, materialId: 'm2', quantity: 2 }, + ], + }); + const result = await service.createSaleOrderPicking( + 'c1', + 'o1', + { sourceLocationId: 'loc-source' }, + 'u1', + ); + expect(result.pickingId).toBe('pk1'); + expect(result.status).toBe('DRAFT'); + expect(result.moveCount).toBe(2); + expect(prisma.stockPicking.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + sourceLocationId: 'loc-source', + moves: expect.objectContaining({ + create: expect.arrayContaining([ + expect.objectContaining({ sourceLocationId: 'loc-source' }), + ]), + }), + }), + }), + ); + }); }); - it('retries no-batch deduction and succeeds on second candidate', async () => { - prisma.stockLocation.findFirst.mockResolvedValue({ - id: 'loc-source', - name: 'L1', - warehouseId: 'w1', + describe('confirmStockPicking', () => { + it('throws when picking not found', async () => { + prisma.stockPicking.findFirst.mockResolvedValue(null); + await expect( + service.confirmStockPicking('c1', 'pk1', {}, 'u1'), + ).rejects.toBeInstanceOf(NotFoundException); }); - prisma.material.findFirst.mockResolvedValue({ id: 'm1', unitPrice: 10 }); - tx.stockQuant.findFirst - .mockResolvedValueOnce({ id: 'q1', batchNo: 'B1' }) - .mockResolvedValueOnce({ id: 'q2', batchNo: 'B2' }); + it('skips when picking is already DONE (idempotent)', async () => { + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DONE', + moves: [], + }); + const result = await service.confirmStockPicking('c1', 'pk1', {}, 'u1'); + expect(result.status).toBe('DONE'); + expect(result.message).toContain('跳过重复处理'); + expect(prisma.$transaction).not.toHaveBeenCalled(); + }); - tx.stockQuant.updateMany - .mockResolvedValueOnce({ count: 0 }) - .mockResolvedValueOnce({ count: 1 }); + it('throws when picking is CANCELLED', async () => { + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'CANCELLED', + moves: [], + }); + await expect( + service.confirmStockPicking('c1', 'pk1', {}, 'u1'), + ).rejects.toBeInstanceOf(BadRequestException); + }); - tx.inventoryTransaction.create.mockResolvedValue({ - id: 't1', - type: 'OUTBOUND', - materialId: 'm1', - quantity: 2, - referenceNo: 'R1', + it('throws before posting when an executable outbound line has no source location', async () => { + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DRAFT', + moves: [ + { + id: 'mv1', + lineNo: 1, + materialId: 'm1', + quantity: 2, + status: 'DRAFT', + sourceLocationId: null, + destLocationId: null, + batchNo: null, + }, + ], + }); + await expect( + service.confirmStockPicking('c1', 'pk1', {}, 'u1'), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.$transaction).not.toHaveBeenCalled(); }); - const result: { id: string } = (await service.createStockMove( - 'c1', - { - sourceLocationId: 'loc-source', + it('posts stock once for executable lines and links transactions to moves', async () => { + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DRAFT', + referenceType: 'SALE_ORDER', + referenceId: 'o1', + moves: [ + { + id: 'mv1', + lineNo: 1, + materialId: 'm1', + quantity: 2, + status: 'DRAFT', + sourceLocationId: 'loc-source', + destLocationId: null, + batchNo: 'B1', + }, + ], + }); + tx.stockMove.updateMany.mockResolvedValue({ count: 1 }); + tx.stockQuant.updateMany.mockResolvedValue({ count: 1 }); + tx.inventoryTransaction.create.mockResolvedValue({ + id: 'tx1', + type: 'OUTBOUND', materialId: 'm1', quantity: 2, - referenceNo: 'R1', - }, - 'u1', - )) as unknown as { id: string }; - - type CreateCall = { - data: { - batchNo: string; - }; - }; - - const createCalls = tx.inventoryTransaction.create.mock - .calls as unknown as Array<[CreateCall]>; - const createCall = createCalls[0]?.[0]; - - expect(result.id).toBe('t1'); - expect(tx.stockQuant.findFirst).toHaveBeenCalledTimes(2); - expect(tx.stockQuant.updateMany).toHaveBeenCalledTimes(2); - expect(createCall.data.batchNo).toBe('B2'); - expect(eventEmitter.emit).toHaveBeenCalledWith( - 'inventory.stock_depleted', - expect.objectContaining({ materialId: 'm1', quantity: 2 }), - ); + referenceNo: 'PICKING-PICK-001', + }); + + const result = await service.confirmStockPicking('c1', 'pk1', {}, 'u1'); + + expect(result.status).toBe('DONE'); + expect(result.confirmedLines).toHaveLength(1); + expect(tx.stockMove.updateMany).toHaveBeenCalledWith({ + where: { id: 'mv1', status: { in: ['DRAFT', 'CONFIRMED'] } }, + data: { status: 'CONFIRMED' }, + }); + expect(tx.inventoryTransaction.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + stockMoveId: 'mv1', + sourceLocationId: 'loc-source', + type: 'OUTBOUND', + }), + }), + ); + expect(tx.order.update).toHaveBeenCalledWith({ + where: { id: 'o1' }, + data: { status: 'SHIPPED' }, + }); + }); + + it('skips line posting when another confirm already claimed the move', async () => { + prisma.stockPicking.findFirst.mockResolvedValue({ + id: 'pk1', + pickingNo: 'PICK-001', + status: 'DRAFT', + moves: [ + { + id: 'mv1', + lineNo: 1, + materialId: 'm1', + quantity: 2, + status: 'DRAFT', + sourceLocationId: 'loc-source', + destLocationId: null, + batchNo: 'B1', + }, + ], + }); + tx.stockMove.updateMany.mockResolvedValue({ count: 0 }); + + const result = await service.confirmStockPicking('c1', 'pk1', {}, 'u1'); + + expect(result.confirmedLines).toHaveLength(0); + expect(result.skippedLines).toHaveLength(1); + expect(tx.inventoryTransaction.create).not.toHaveBeenCalled(); + }); + }); + + describe('getPickings', () => { + it('filters list by normalized status', async () => { + prisma.stockPicking.findMany.mockResolvedValue([]); + prisma.stockPicking.count.mockResolvedValue(0); + await service.getPickings('c1', { page: 1, limit: 10 }, 'done'); + expect(prisma.stockPicking.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { companyId: 'c1', status: 'DONE' }, + }), + ); + }); + + it('rejects unsupported list status values', async () => { + await expect( + service.getPickings('c1', { page: 1, limit: 10 }, 'bad'), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.stockPicking.findMany).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/api/src/inventory/inventory.service.ts b/apps/api/src/inventory/inventory.service.ts index d8504b4..cfac9ae 100644 --- a/apps/api/src/inventory/inventory.service.ts +++ b/apps/api/src/inventory/inventory.service.ts @@ -5,11 +5,13 @@ import { ConflictException, } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { Prisma } from '@prisma/client'; +import { MoveStatus, PickingStatus, Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { KyselyService } from '../core/prisma/kysely.service'; import { PaginationDto } from '../core/dto/pagination.dto'; import { + ConfirmPickingDto, + CreatePickingDto, CreateStockMoveDto, PurchaseInboundPostingDto, ReversePurchaseInboundDto, @@ -343,97 +345,340 @@ export class InventoryService { }; } + /** + * 销售订单发货(便捷方法):创建拣货单 + 确认,一步完成。 + * 内部委托给 createSaleOrderPicking + confirmStockPicking,保持幂等。 + */ async postSaleOrderShipment( companyId: string, orderId: string, payload: SaleOrderShipmentDto, operatorId?: string, ) { + const pickingResult = await this.createSaleOrderPicking( + companyId, + orderId, + { sourceLocationId: payload.sourceLocationId }, + operatorId, + ); + + const confirmResult = await this.confirmStockPicking( + companyId, + pickingResult.pickingId, + { note: payload.note ?? `销售订单自动出库:${orderId}` }, + operatorId, + ); + + return { + orderId, + pickingId: pickingResult.pickingId, + pickingNo: pickingResult.pickingNo, + status: confirmResult.status, + confirmedLines: confirmResult.confirmedLines ?? [], + message: confirmResult.message, + }; + } + + async postPurchaseInbound( + companyId: string, + payload: PurchaseInboundPostingDto, + operatorId?: string, + ) { + const referenceNo = `PURCHASE-IN-${payload.purchaseNo}`; + const existedMove = await this.prisma.inventoryTransaction.findFirst({ + where: { + companyId, + referenceNo, + materialId: payload.materialId, + type: 'INBOUND', + }, + select: { id: true }, + }); + + if (existedMove) { + return { + referenceNo, + transactionId: existedMove.id, + message: '采购单入库已过账,跳过重复处理', + }; + } + + const transaction = await this.createStockMove( + companyId, + { + materialId: payload.materialId, + destLocationId: payload.destLocationId, + quantity: payload.quantity, + batchNo: payload.batchNo, + referenceNo, + documentType: 'PURCHASE_ORDER', + documentId: payload.purchaseNo, + note: payload.note ?? `采购自动入库:${payload.purchaseNo}`, + }, + operatorId, + ); + + return { + referenceNo, + transactionId: transaction.id, + message: '采购单自动入库过账完成', + }; + } + + // ============================================================= + // Stock Picking: 销售发货改造 + // ============================================================= + + async createSaleOrderPicking( + companyId: string, + orderId: string, + payload: CreatePickingDto, + _operatorId?: string, + ) { + void _operatorId; + const sourceLocation = await this.resolveLocationOwnership( + companyId, + payload.sourceLocationId, + '来源库位', + ); + const destLocation = await this.resolveLocationOwnership( + companyId, + payload.destLocationId, + '目标库位', + ); + + if (!sourceLocation?.id) { + throw new BadRequestException('销售出库拣货单必须指定来源库位'); + } + + if (destLocation?.id && sourceLocation.id === destLocation.id) { + throw new BadRequestException('来源库位和目标库位不能相同'); + } + const order = await this.prisma.order.findFirst({ where: { id: orderId, companyId }, include: { items: true }, }); - if (!order) { - throw new NotFoundException('销售订单不存在或无权限访问'); - } + if (!order) throw new NotFoundException('销售订单不存在或无权限访问'); + if (!order.items.length) + throw new BadRequestException('销售订单无明细,无法创建拣货单'); - if (!order.items.length) { - throw new BadRequestException('销售订单无明细,无法自动过账'); + const existingPicking = await this.prisma.stockPicking.findFirst({ + where: { + companyId, + referenceType: 'SALE_ORDER', + referenceId: orderId, + status: { notIn: [PickingStatus.CANCELLED] }, + }, + }); + + if (existingPicking) { + return { + pickingId: existingPicking.id, + pickingNo: existingPicking.pickingNo, + status: existingPicking.status, + message: '该订单已存在拣货单,已返回', + }; } const materialQuantityMap = new Map(); - for (const item of order.items) { const product = await this.prisma.product.findFirst({ where: { id: item.productId, companyId }, select: { id: true, materialId: true, name: true }, }); - - if (!product) { + if (!product) throw new BadRequestException(`订单项产品不存在:${item.productId}`); - } - - if (!product.materialId) { + if (!product.materialId) throw new BadRequestException( - `产品 ${product.name} 未绑定主物料,无法自动过账`, + `产品 ${product.name} 未绑定主物料,无法创建拣货单`, ); - } - const current = materialQuantityMap.get(product.materialId) ?? 0; materialQuantityMap.set(product.materialId, current + item.quantity); } - const referenceNo = `SALE-SHIP-${order.orderNo}`; - const existedMoves = await this.prisma.inventoryTransaction.count({ - where: { companyId, referenceNo }, + const now = new Date(); + const pickingNo = `PICK-${now.getFullYear()}${String( + now.getMonth() + 1, + ).padStart(2, '0')}-${Math.floor(1000 + Math.random() * 9000)}`; + const picking = await this.prisma.stockPicking.create({ + data: { + pickingNo, + type: 'OUTBOUND', + referenceType: 'SALE_ORDER', + referenceId: orderId, + scheduledDate: payload.scheduledDate + ? new Date(payload.scheduledDate) + : null, + sourceLocationId: sourceLocation.id, + destLocationId: destLocation?.id ?? null, + status: PickingStatus.DRAFT, + companyId, + moves: { + create: Array.from(materialQuantityMap.entries()).map( + ([materialId, quantity], index) => ({ + lineNo: index + 1, + materialId, + sourceLocationId: sourceLocation.id, + destLocationId: destLocation?.id ?? null, + quantity, + status: MoveStatus.DRAFT, + companyId, + }), + ), + }, + }, + include: { moves: true }, }); - if (existedMoves > 0) { + return { + pickingId: picking.id, + pickingNo: picking.pickingNo, + status: picking.status, + moveCount: picking.moves.length, + message: '拣货单已创建,状态为 DRAFT', + }; + } + async confirmStockPicking( + companyId: string, + pickingId: string, + payload: ConfirmPickingDto, + operatorId?: string, + ) { + const picking = await this.prisma.stockPicking.findFirst({ + where: { id: pickingId, companyId }, + include: { moves: { orderBy: { lineNo: 'asc' } } }, + }); + + if (!picking) throw new NotFoundException('拣货单不存在或无权限访问'); + + if (picking.status === PickingStatus.DONE) { return { - orderId: order.id, - orderNo: order.orderNo, - postedLines: [], - message: '销售订单已完成过账,已跳过重复处理', + pickingId: picking.id, + pickingNo: picking.pickingNo, + status: picking.status, + message: '拣货单已确认完成,跳过重复处理', }; } - const moveNote = payload.note ?? `销售订单自动出库:${order.orderNo}`; - const results = await this.prisma.$transaction(async (tx) => { - const postedLines: Array<{ - materialId: string; - quantity: number; - transactionId: string; - }> = []; + if (picking.status === PickingStatus.CANCELLED) + throw new BadRequestException('拣货单已取消,无法确认'); + if (!picking.moves.length) + throw new BadRequestException('拣货单无明细行,无法确认'); + + for (const move of picking.moves) { + if ( + move.status !== MoveStatus.CANCELLED && + move.status !== MoveStatus.DONE && + !move.sourceLocationId + ) { + throw new BadRequestException( + `拣货单行 ${move.lineNo} 缺少来源库位,无法确认出库`, + ); + } + if ( + move.sourceLocationId && + move.destLocationId && + move.sourceLocationId === move.destLocationId + ) { + throw new BadRequestException( + `拣货单行 ${move.lineNo} 来源库位和目标库位不能相同`, + ); + } + } + + const referenceNo = `PICKING-${picking.pickingNo}`; + const note = payload.note ?? `拣货单确认出库:${picking.pickingNo}`; + const confirmedLines: Array<{ + moveId: string; + materialId: string; + quantity: number; + transactionId: string; + }> = []; + const skippedLines: Array<{ + moveId: string; + materialId: string; + message: string; + }> = []; - for (const [materialId, quantity] of materialQuantityMap.entries()) { + await this.prisma.$transaction(async (tx) => { + for (const move of picking.moves) { + if (move.status === MoveStatus.DONE) { + skippedLines.push({ + moveId: move.id, + materialId: move.materialId, + message: '该行已执行过,跳过', + }); + continue; + } + if (move.status === MoveStatus.CANCELLED) { + skippedLines.push({ + moveId: move.id, + materialId: move.materialId, + message: '该行已取消,跳过', + }); + continue; + } + + const claimed = await tx.stockMove.updateMany({ + where: { + id: move.id, + status: { in: [MoveStatus.DRAFT, MoveStatus.CONFIRMED] }, + }, + data: { status: MoveStatus.CONFIRMED }, + }); + + if (claimed.count === 0) { + skippedLines.push({ + moveId: move.id, + materialId: move.materialId, + message: '该行已被其他确认流程处理,跳过', + }); + continue; + } + + const lineNote = `${note} (行${move.lineNo})`; const transaction = await this.executeStockMove(tx, { companyId, - sourceLocationId: payload.sourceLocationId, - materialId, - quantity, - batchNo: payload.batchNo, + materialId: move.materialId, + quantity: move.quantity, + sourceLocationId: move.sourceLocationId ?? undefined, + destLocationId: move.destLocationId ?? undefined, + batchNo: move.batchNo ?? undefined, referenceNo, - note: moveNote, + note: lineNote, operatorId: operatorId || 'SYSTEM', + stockMoveId: move.id, + }); + + await tx.stockMove.update({ + where: { id: move.id }, + data: { status: MoveStatus.DONE, quantityDone: move.quantity }, }); - postedLines.push({ - materialId, - quantity, + confirmedLines.push({ + moveId: move.id, + materialId: move.materialId, + quantity: move.quantity, transactionId: transaction.id, }); } - await tx.order.update({ - where: { id: order.id }, - data: { status: 'SHIPPED' }, + await tx.stockPicking.update({ + where: { id: picking.id }, + data: { status: PickingStatus.DONE, completedDate: new Date() }, }); - return postedLines; + if (picking.referenceType === 'SALE_ORDER' && picking.referenceId) { + await tx.order.update({ + where: { id: picking.referenceId }, + data: { status: 'SHIPPED' }, + }); + } }); - for (const line of results) { + for (const line of confirmedLines) { this.eventEmitter.emit('inventory.stock_depleted', { companyId, referenceNo, @@ -444,57 +689,47 @@ export class InventoryService { } return { - orderId: order.id, - orderNo: order.orderNo, - postedLines: results, - message: '销售订单自动过账完成,订单状态已更新为 SHIPPED', + pickingId: picking.id, + pickingNo: picking.pickingNo, + status: PickingStatus.DONE, + confirmedLines, + skippedLines, + message: `拣货单已确认完成,${confirmedLines.length} 行已执行`, }; } - async postPurchaseInbound( + async getPickings( companyId: string, - payload: PurchaseInboundPostingDto, - operatorId?: string, + pagination: PaginationDto, + status?: string, ) { - const referenceNo = `PURCHASE-IN-${payload.purchaseNo}`; - const existedMove = await this.prisma.inventoryTransaction.findFirst({ - where: { - companyId, - referenceNo, - materialId: payload.materialId, - type: 'INBOUND', - }, - select: { id: true }, - }); + const { page = 1, limit = 20 } = pagination; + const where: Prisma.StockPickingWhereInput = { companyId }; + const pickingStatus = this.parsePickingStatus(status); + if (pickingStatus) where.status = pickingStatus; - if (existedMove) { - return { - referenceNo, - transactionId: existedMove.id, - message: '采购单入库已过账,跳过重复处理', - }; - } + const [data, total] = await Promise.all([ + this.prisma.stockPicking.findMany({ + where, + include: { moves: true }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.stockPicking.count({ where }), + ]); - const transaction = await this.createStockMove( - companyId, - { - materialId: payload.materialId, - destLocationId: payload.destLocationId, - quantity: payload.quantity, - batchNo: payload.batchNo, - referenceNo, - documentType: 'PURCHASE_ORDER', - documentId: payload.purchaseNo, - note: payload.note ?? `采购自动入库:${payload.purchaseNo}`, - }, - operatorId, - ); + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } - return { - referenceNo, - transactionId: transaction.id, - message: '采购单自动入库过账完成', - }; + async getPickingById(pickingId: string, companyId: string) { + const picking = await this.prisma.stockPicking.findFirst({ + where: { id: pickingId, companyId }, + include: { moves: { include: { material: true } } }, + }); + + if (!picking) throw new NotFoundException('拣货单不存在或无权限访问'); + return picking; } async reverseSaleOrderShipment( @@ -709,6 +944,21 @@ export class InventoryService { return `BATCH${Date.now()}`; } + private parsePickingStatus(status?: string): PickingStatus | undefined { + if (!status) return undefined; + + const normalized = status.trim().toUpperCase(); + const validStatus = Object.values(PickingStatus).find( + (value) => value === normalized, + ); + + if (!validStatus) { + throw new BadRequestException(`不支持的拣货单状态:${status}`); + } + + return validStatus; + } + private async executeStockMove( tx: Prisma.TransactionClient, input: { @@ -721,6 +971,7 @@ export class InventoryService { operatorId: string; referenceNo?: string; note?: string; + stockMoveId?: string; }, ): Promise { let finalBatchNo = input.batchNo; @@ -779,6 +1030,7 @@ export class InventoryService { companyId: input.companyId, referenceNo: input.referenceNo, note: input.note, + stockMoveId: input.stockMoveId, }, }); } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 04b23df..380bf54 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -101,20 +101,21 @@ async function bootstrap() { // 配置 Swagger const config = new DocumentBuilder() - .setTitle('EIP API') - .setDescription('ERP/EIP 后端接口文档') + .setTitle('OneERP API') + .setDescription('OneERP 后端接口文档') .setVersion('1.0') .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); + const port = Number(process.env.PORT ?? 8000); + // 监听 0.0.0.0 使暴露在内网网段 - // 确保端口为 8000 避免和其他默认应用冲突 - await app.listen(process.env.PORT ?? 8000, '0.0.0.0'); + await app.listen(port, '0.0.0.0'); console.log( - `EIP 核心服务已运行,内网任意设备均可通过 http://[主机局域网IP]:8000/api 访问 API`, + `OneERP API 已运行,内网任意设备均可通过 http://[主机局域网IP]:${port}/api 访问 API`, ); - console.log(`Swagger 接口文档地址: http://localhost:8000/api/docs`); + console.log(`Swagger 接口文档地址: http://localhost:${port}/api/docs`); } void bootstrap(); diff --git a/apps/api/src/orders/dto/create-order.dto.ts b/apps/api/src/orders/dto/create-order.dto.ts index 6dc6dde..e26540b 100644 --- a/apps/api/src/orders/dto/create-order.dto.ts +++ b/apps/api/src/orders/dto/create-order.dto.ts @@ -44,7 +44,9 @@ export class CreateOrderDto { @Type(() => OrderItemDto) items!: OrderItemDto[]; - @ApiPropertyOptional({ description: '订单税码ID (建议填写,未填将使用默认税码)' }) + @ApiPropertyOptional({ + description: '订单税码ID (建议填写,未填将使用默认税码)', + }) @IsOptional() @IsString() taxCodeId?: string; diff --git a/apps/api/src/orders/orders.controller.ts b/apps/api/src/orders/orders.controller.ts index d553247..1be2db1 100644 --- a/apps/api/src/orders/orders.controller.ts +++ b/apps/api/src/orders/orders.controller.ts @@ -73,6 +73,15 @@ export class OrdersController { return this.ordersService.getOrderById(orderId, companyId); } + @Get(':id/stock-transactions') + @ApiOperation({ summary: '获取订单关联的库存过账与发货记录' }) + async getOrderStockTransactions( + @Param('id') orderId: string, + @CurrentCompany() companyId: string, + ) { + return this.ordersService.getOrderStockTransactions(orderId, companyId); + } + @Get(':id/timeline') @ApiOperation({ summary: '获取订单时间线(审计与流转记录)' }) async getOrderTimeline( diff --git a/apps/api/src/orders/orders.service.ts b/apps/api/src/orders/orders.service.ts index b5fa42d..c268c69 100644 --- a/apps/api/src/orders/orders.service.ts +++ b/apps/api/src/orders/orders.service.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; +import { TaxService } from '../core/tax/tax.service'; import { PaginationDto } from '../core/dto/pagination.dto'; import { EventQueueService } from '../core/events/event-queue.service'; @@ -32,86 +33,42 @@ export class OrdersService { constructor( private prisma: PrismaService, private readonly eventQueueService: EventQueueService, + private readonly taxService: TaxService, ) {} - private round2(value: number) { - return Math.round((value + Number.EPSILON) * 100) / 100; - } - - private calcTaxBreakdown( - baseAmount: number, - taxRate: number, - isTaxInclusive: boolean, - ) { - const safeRate = Math.max(0, Math.min(1, Number(taxRate ?? 0))); - if (isTaxInclusive) { - const subTotal = this.round2(baseAmount / (1 + safeRate)); - const taxAmount = this.round2(baseAmount - subTotal); - return { - subTotal, - taxAmount, - total: this.round2(baseAmount), - }; - } - - const subTotal = this.round2(baseAmount); - const taxAmount = this.round2(subTotal * safeRate); - const total = this.round2(subTotal + taxAmount); - return { subTotal, taxAmount, total }; - } - - private async resolveTaxCode(companyId: string, taxCodeId?: string | null) { - if (taxCodeId) { - const taxCode = await this.prisma.taxCode.findFirst({ - where: { id: taxCodeId, companyId, active: true }, - }); - if (!taxCode) { - throw new BadRequestException('税码不存在或已停用,请确认税码选择'); - } - return { ...taxCode, isFallback: false }; - } - - const defaultTaxCode = await this.prisma.taxCode.findFirst({ - where: { companyId, isDefault: true, active: true }, - orderBy: { updatedAt: 'desc' }, - }); - if (defaultTaxCode) { - return { ...defaultTaxCode, isFallback: false }; - } - - this.logger.warn( - `未配置默认税码,订单将使用 13% 默认税率: companyId=${companyId}`, - ); - return { - id: null, - rate: 0.13, - isTaxInclusive: true, - isFallback: true, - }; - } - async createOrder(companyId: string, userId: string, data: CreateOrderInput) { - const { partnerId, items, aiSummary, expectedDate, notes, taxCodeId } = data; + const { partnerId, items, aiSummary, expectedDate, notes, taxCodeId } = + data; - // 自动生成订单号 - const orderNo = `ORD-${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}-${Math.floor(1000 + Math.random() * 9000)}`; + const orderNo = + 'ORD-' + + new Date().getFullYear() + + String(new Date().getMonth() + 1).padStart(2, '0') + + '-' + + Math.floor(1000 + Math.random() * 9000); let totalAmount = 0; let subTotal = 0; let taxTotal = 0; - const baseTaxCode = await this.resolveTaxCode(companyId, taxCodeId); + const baseTaxCode = await this.taxService.resolveTaxCode( + companyId, + taxCodeId, + { operatorId: userId, entity: 'Order', entityId: 'new' }, + ); + const orderItems = await Promise.all( (items ?? []).map(async (item) => { const resolvedTaxCode = item.taxCodeId - ? await this.resolveTaxCode(companyId, item.taxCodeId) + ? await this.taxService.resolveTaxCode(companyId, item.taxCodeId) : baseTaxCode; const lineBase = item.quantity * item.unitPrice; - const breakdown = this.calcTaxBreakdown( + const breakdown = this.taxService.calcTaxBreakdown( lineBase, resolvedTaxCode.rate, resolvedTaxCode.isTaxInclusive, + resolvedTaxCode.taxNature, ); totalAmount += breakdown.total; @@ -125,7 +82,7 @@ export class OrdersService { totalPrice: breakdown.total, subTotal: breakdown.subTotal, taxAmount: breakdown.taxAmount, - taxRate: Number(resolvedTaxCode.rate ?? 0), + taxRate: breakdown.taxRate, taxCodeId: resolvedTaxCode.id ?? null, }; }), @@ -139,8 +96,8 @@ export class OrdersService { partnerId, status: 'DRAFT', totalAmount, - subTotal: this.round2(subTotal), - taxTotal: this.round2(taxTotal), + subTotal: this.taxService.round2(subTotal), + taxTotal: this.taxService.round2(taxTotal), taxCodeId: baseTaxCode.id ?? null, aiSummary, expectedDate: expectedDate ? new Date(expectedDate) : null, @@ -157,7 +114,7 @@ export class OrdersService { await this.eventQueueService.publish({ eventName: 'order.created', - idempotencyKey: `order_created:${created.id}`, + idempotencyKey: 'order_created:' + created.id, companyId, payload: { orderId: created.id, @@ -236,6 +193,61 @@ export class OrdersService { return this.prisma.order.delete({ where: { id: orderId } }); } + async getOrderStockTransactions(orderId: string, companyId: string) { + const order = await this.prisma.order.findFirst({ + where: { id: orderId, companyId }, + select: { id: true, orderNo: true }, + }); + + if (!order) { + throw new NotFoundException('该订单不存在或您无权查看'); + } + + // 库存过账时 referenceNo 格式: SALE-SHIP-{orderNo} 或 REVERSE-SHIP-{orderNo} + const referencePatterns = [ + `SALE-SHIP-${order.orderNo}`, + `REVERSE-SHIP-${order.orderNo}`, + order.orderNo, + ]; + + const transactions = await this.prisma.inventoryTransaction.findMany({ + where: { + companyId, + referenceNo: { in: referencePatterns }, + }, + include: { + material: { select: { id: true, sku: true, name: true, unit: true } }, + sourceLocation: { + select: { id: true, name: true, code: true }, + include: { warehouse: { select: { id: true, name: true } } }, + }, + destLocation: { + select: { id: true, name: true, code: true }, + include: { warehouse: { select: { id: true, name: true } } }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + return { + orderId: order.id, + orderNo: order.orderNo, + transactions: transactions.map((tx) => ({ + id: tx.id, + type: tx.type, + materialId: tx.materialId, + material: tx.material, + quantity: tx.quantity, + batchNo: tx.batchNo, + referenceNo: tx.referenceNo, + note: tx.note, + sourceLocation: tx.sourceLocation, + destLocation: tx.destLocation, + createdAt: tx.createdAt, + })), + }; + } + async getOrderTimeline(orderId: string, companyId: string) { const order = await this.prisma.order.findFirst({ where: { id: orderId, companyId }, diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts index f766667..f1062a3 100644 --- a/apps/api/src/prisma/prisma.service.ts +++ b/apps/api/src/prisma/prisma.service.ts @@ -17,31 +17,34 @@ function isRecord(value: unknown): value is Record { } const COMPANY_SCOPED_MODELS = new Set([ - 'Order', - 'OrderItem', + 'UserCompanyRole', + 'Department', 'Partner', + 'TaxCode', + 'Order', 'Warehouse', 'StockLocation', - 'StockQuant', 'Material', 'ProductCategory', 'Product', 'Bom', - 'BomLine', + 'CustomFieldDefinition', 'FileRecord', 'InventoryTransaction', + 'Workflow', 'WorkOrder', 'Invoice', - 'Workflow', - 'WorkflowState', - 'WorkflowTransition', - 'CustomFieldDefinition', + 'PurchaseInvoice', 'Account', 'Journal', 'JournalEntry', 'EventDlq', 'AuditLog', - 'Department', + 'PurchaseOrder', + 'PurchaseOrderLine', + 'GoodsReceipt', + 'StockPicking', + 'StockMove', ]); @Injectable() diff --git a/apps/api/src/purchase-orders/dto/create-purchase-order.dto.ts b/apps/api/src/purchase-orders/dto/create-purchase-order.dto.ts new file mode 100644 index 0000000..5d3c43c --- /dev/null +++ b/apps/api/src/purchase-orders/dto/create-purchase-order.dto.ts @@ -0,0 +1,80 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsString, + IsNotEmpty, + IsNumber, + Min, + IsArray, + ValidateNested, + IsOptional, + IsDateString, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +// ---- 行项 DTO ---- + +export class AddPurchaseOrderLineDto { + @ApiPropertyOptional({ + description: '物料ID(materialId 与 productId 二选一)', + }) + @IsOptional() + @IsString() + materialId?: string; + + @ApiPropertyOptional({ + description: '产品ID(materialId 与 productId 二选一)', + }) + @IsOptional() + @IsString() + productId?: string; + + @ApiPropertyOptional({ description: '行描述' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ description: '数量' }) + @IsNumber() + @Min(0.01) + quantity!: number; + + @ApiProperty({ description: '单价' }) + @IsNumber() + @Min(0) + unitPrice!: number; + + @ApiPropertyOptional({ description: '税率 (0~1),默认 0.13' }) + @IsOptional() + @IsNumber() + @Min(0) + taxRate?: number; +} + +// ---- 创建采购单 DTO ---- + +export class CreatePurchaseOrderDto { + @ApiProperty({ description: '供应商ID (partnerId)' }) + @IsString() + @IsNotEmpty() + partnerId!: string; + + @ApiPropertyOptional({ description: '预计到货日期' }) + @IsOptional() + @IsDateString() + expectedDate?: string; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + notes?: string; + + @ApiPropertyOptional({ + description: '初始行项列表(可选,也可后续通过添加行项接口补充)', + type: [AddPurchaseOrderLineDto], + }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AddPurchaseOrderLineDto) + lines?: AddPurchaseOrderLineDto[]; +} diff --git a/apps/api/src/purchase-orders/purchase-orders.controller.ts b/apps/api/src/purchase-orders/purchase-orders.controller.ts new file mode 100644 index 0000000..94e7e1c --- /dev/null +++ b/apps/api/src/purchase-orders/purchase-orders.controller.ts @@ -0,0 +1,134 @@ +import { + Controller, + Post, + Get, + Delete, + Body, + Param, + UseGuards, + Query, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { PurchaseOrdersService } from './purchase-orders.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; +import { PermissionsGuard } from '../core/guards/permissions.guard'; +import { RequirePermissions } from '../core/decorators/require-permissions.decorator'; +import { CurrentCompany } from '../core/decorators/current-company.decorator'; +import { CurrentUser } from '../core/decorators/current-user.decorator'; +import { + CreatePurchaseOrderDto, + AddPurchaseOrderLineDto, +} from './dto/create-purchase-order.dto'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import type { JwtUserPayload } from '../core/http/request.types'; + +@ApiTags('采购管理 (Purchase Orders)') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, TenantGuard, PermissionsGuard) +@Controller('purchase-orders') +export class PurchaseOrdersController { + constructor(private readonly poService: PurchaseOrdersService) {} + + @Post() + @ApiOperation({ summary: '创建采购单' }) + @RequirePermissions('purchase:write') + async create( + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Body() dto: CreatePurchaseOrderDto, + ) { + return this.poService.createPurchaseOrder(companyId, user.id, dto); + } + + @Get() + @ApiOperation({ summary: '获取采购单列表(支持分页和搜索)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'search', required: false, type: String }) + @ApiQuery({ name: 'status', required: false, type: String }) + async findAll( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + @Query('search') search?: string, + @Query('status') status?: string, + ) { + return this.poService.getPurchaseOrders( + companyId, + pagination, + search, + status, + ); + } + + @Get(':id') + @ApiOperation({ summary: '获取采购单详情' }) + async findOne(@Param('id') id: string, @CurrentCompany() companyId: string) { + return this.poService.getPurchaseOrderById(id, companyId); + } + + @Post(':id/lines') + @ApiOperation({ summary: '向采购单添加行项(仅草稿状态)' }) + async addLine( + @Param('id') id: string, + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Body() dto: AddPurchaseOrderLineDto, + ) { + return this.poService.addLine(id, companyId, user.id, dto); + } + + @Post(':id/submit') + @ApiOperation({ summary: '提交采购单审核 (DRAFT → SUBMITTED)' }) + @RequirePermissions('purchase:write') + async submit( + @Param('id') id: string, + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + ) { + return this.poService.submit(id, companyId, user.id); + } + + @Post(':id/confirm') + @ApiOperation({ + summary: '确认采购单 (SUBMITTED → APPROVED),确认后可生成收货单', + }) + @RequirePermissions('purchase:approve') + async confirm( + @Param('id') id: string, + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + ) { + return this.poService.confirm(id, companyId, user.id); + } + + @Delete(':id') + @ApiOperation({ summary: '删除草稿采购单' }) + @RequirePermissions('purchase:write') + async remove( + @Param('id') id: string, + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + ) { + return this.poService.delete(id, companyId, user.id); + } + + @Post(':id/cancel') + @ApiOperation({ + summary: '取消采购单 (DRAFT/SUBMITTED/APPROVED → CANCELLED)', + }) + @RequirePermissions('purchase:cancel', 'purchase:write') + async cancel( + @Param('id') id: string, + @CurrentCompany() companyId: string, + @CurrentUser() user: JwtUserPayload, + @Body() body?: { reason?: string }, + ) { + return this.poService.cancel(id, companyId, user.id, body?.reason); + } +} diff --git a/apps/api/src/purchase-orders/purchase-orders.module.ts b/apps/api/src/purchase-orders/purchase-orders.module.ts new file mode 100644 index 0000000..97703aa --- /dev/null +++ b/apps/api/src/purchase-orders/purchase-orders.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PurchaseOrdersController } from './purchase-orders.controller'; +import { PurchaseOrdersService } from './purchase-orders.service'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [PurchaseOrdersController], + providers: [PurchaseOrdersService], + exports: [PurchaseOrdersService], +}) +export class PurchaseOrdersModule {} diff --git a/apps/api/src/purchase-orders/purchase-orders.service.ts b/apps/api/src/purchase-orders/purchase-orders.service.ts new file mode 100644 index 0000000..7ae8d04 --- /dev/null +++ b/apps/api/src/purchase-orders/purchase-orders.service.ts @@ -0,0 +1,433 @@ +import { + Injectable, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { Prisma, PurchaseOrderStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { + CreatePurchaseOrderDto, + AddPurchaseOrderLineDto, +} from './dto/create-purchase-order.dto'; + +@Injectable() +export class PurchaseOrdersService { + private readonly logger = new Logger(PurchaseOrdersService.name); + + constructor(private readonly prisma: PrismaService) {} + + private round2(value: number) { + return Math.round((value + Number.EPSILON) * 100) / 100; + } + + private calcLineAmounts( + quantity: number, + unitPrice: number, + taxRate: number, + ) { + const safeRate = Math.max(0, Math.min(1, Number(taxRate ?? 0))); + const subTotal = this.round2(quantity * unitPrice); + const taxAmount = this.round2(subTotal * safeRate); + const totalAmount = this.round2(subTotal + taxAmount); + return { subTotal, taxAmount, totalAmount, taxRate: safeRate }; + } + + private async recalcOrderTotals(orderId: string) { + const lines = await this.prisma.purchaseOrderLine.findMany({ + where: { orderId }, + }); + const subTotal = this.round2( + lines.reduce((s, l) => s + Number(l.subTotal), 0), + ); + const taxTotal = this.round2( + lines.reduce((s, l) => s + Number(l.taxAmount), 0), + ); + const totalAmount = this.round2( + lines.reduce((s, l) => s + Number(l.totalAmount), 0), + ); + return this.prisma.purchaseOrder.update({ + where: { id: orderId }, + data: { subTotal, taxTotal, totalAmount }, + }); + } + + private async nextLineNo(orderId: string): Promise { + const last = await this.prisma.purchaseOrderLine.findFirst({ + where: { orderId }, + orderBy: { lineNo: 'desc' }, + select: { lineNo: true }, + }); + return (last?.lineNo ?? 0) + 1; + } + + private generateOrderNo(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const seq = Math.floor(1000 + Math.random() * 9000); + return `PO-${year}${month}-${seq}`; + } + + private auditLog( + userId: string, + action: string, + entityId: string, + details: Prisma.InputJsonValue, + companyId: string, + ) { + return this.prisma.auditLog.create({ + data: { + userId, + action, + entity: 'purchase_order', + entityId, + details, + companyId, + }, + }); + } + + // ======================================= + // 创建采购单 + // ======================================= + async createPurchaseOrder( + companyId: string, + userId: string, + dto: CreatePurchaseOrderDto, + ) { + const { partnerId, lines, expectedDate, notes } = dto; + + const partner = await this.prisma.partner.findFirst({ + where: { id: partnerId, companyId, type: { in: ['SUPPLIER', 'BOTH'] } }, + }); + if (!partner) { + throw new BadRequestException('供应商不存在或不是供应商类型'); + } + + const orderNo = this.generateOrderNo(); + let subTotal = 0; + let taxTotal = 0; + let totalAmount = 0; + + const lineCreates = (lines ?? []).map((line, index) => { + const taxRate = line.taxRate ?? 0.13; + const amounts = this.calcLineAmounts( + line.quantity, + line.unitPrice, + taxRate, + ); + subTotal += amounts.subTotal; + taxTotal += amounts.taxAmount; + totalAmount += amounts.totalAmount; + return { + lineNo: index + 1, + materialId: line.materialId ?? null, + productId: line.productId ?? null, + description: line.description ?? null, + quantity: line.quantity, + unitPrice: line.unitPrice, + taxRate: amounts.taxRate, + taxAmount: amounts.taxAmount, + subTotal: amounts.subTotal, + totalAmount: amounts.totalAmount, + companyId, + }; + }); + + const created = await this.prisma.purchaseOrder.create({ + data: { + orderNo, + partnerId, + expectedDate: expectedDate ? new Date(expectedDate) : null, + status: 'DRAFT', + subTotal: this.round2(subTotal), + taxTotal: this.round2(taxTotal), + totalAmount: this.round2(totalAmount), + notes, + companyId, + lines: { create: lineCreates }, + }, + include: { lines: true, partner: true }, + }); + + await this.auditLog( + userId, + 'CREATE_PURCHASE_ORDER', + created.id, + { orderNo: created.orderNo, partnerId, lineCount: lineCreates.length }, + companyId, + ); + + return created; + } + + // ======================================= + // 查询采购单列表 + // ======================================= + async getPurchaseOrders( + companyId: string, + pagination: PaginationDto, + search?: string, + status?: string, + ) { + const { page = 1, limit = 20 } = pagination; + const where: Prisma.PurchaseOrderWhereInput = { companyId }; + if (status && status in PurchaseOrderStatus) { + where.status = status as PurchaseOrderStatus; + } + if (search) { + where.OR = [ + { orderNo: { contains: search, mode: 'insensitive' } }, + { partner: { name: { contains: search, mode: 'insensitive' } } }, + ]; + } + + const [data, total] = await Promise.all([ + this.prisma.purchaseOrder.findMany({ + where, + include: { + partner: { select: { id: true, name: true, code: true } }, + lines: true, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.purchaseOrder.count({ where }), + ]); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } + + // ======================================= + // 查询采购单详情 + // ======================================= + async getPurchaseOrderById(orderId: string, companyId: string) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + include: { + partner: true, + lines: { + include: { + material: { select: { id: true, sku: true, name: true } }, + product: { select: { id: true, sku: true, name: true } }, + }, + orderBy: { lineNo: 'asc' }, + }, + receipts: true, + }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权查看'); + return po; + } + + // ======================================= + // 添加行项 (仅 DRAFT 状态) + // ======================================= + async addLine( + orderId: string, + companyId: string, + userId: string, + dto: AddPurchaseOrderLineDto, + ) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + if (po.status !== 'DRAFT') { + throw new BadRequestException('只能向草稿状态的采购单添加行项'); + } + if (!dto.materialId && !dto.productId) { + throw new BadRequestException('materialId 或 productId 至少填写一个'); + } + + const taxRate = dto.taxRate ?? 0.13; + const amounts = this.calcLineAmounts(dto.quantity, dto.unitPrice, taxRate); + const lineNo = await this.nextLineNo(orderId); + + const line = await this.prisma.purchaseOrderLine.create({ + data: { + orderId, + lineNo, + materialId: dto.materialId ?? null, + productId: dto.productId ?? null, + description: dto.description ?? null, + quantity: dto.quantity, + unitPrice: dto.unitPrice, + taxRate: amounts.taxRate, + taxAmount: amounts.taxAmount, + subTotal: amounts.subTotal, + totalAmount: amounts.totalAmount, + companyId, + }, + }); + + await this.recalcOrderTotals(orderId); + await this.auditLog( + userId, + 'ADD_PO_LINE', + orderId, + { + lineNo, + materialId: dto.materialId, + productId: dto.productId, + }, + companyId, + ); + + return line; + } + + // ======================================= + // 提交审核 (DRAFT → SUBMITTED) + // ======================================= + async submit(orderId: string, companyId: string, userId: string) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + include: { lines: true }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + if (po.status !== 'DRAFT') { + throw new BadRequestException('只有草稿状态的采购单才能提交审核'); + } + if (po.lines.length === 0) { + throw new BadRequestException('采购单至少需要一行行项才能提交'); + } + + const updated = await this.prisma.purchaseOrder.update({ + where: { id: orderId }, + data: { status: 'SUBMITTED' }, + include: { lines: true, partner: true }, + }); + + await this.auditLog( + userId, + 'SUBMIT_PURCHASE_ORDER', + orderId, + { + orderNo: po.orderNo, + lineCount: po.lines.length, + totalAmount: po.totalAmount, + }, + companyId, + ); + + return updated; + } + + // ======================================= + // 确认采购单 (SUBMITTED → APPROVED) + // 确认后可生成收货单 + // ======================================= + async confirm(orderId: string, companyId: string, userId: string) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + include: { lines: true }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + if (po.status !== 'SUBMITTED') { + throw new BadRequestException('只有已提交审核的采购单才能确认'); + } + + const updated = await this.prisma.purchaseOrder.update({ + where: { id: orderId }, + data: { status: 'APPROVED' }, + include: { lines: true, partner: true }, + }); + + await this.auditLog( + userId, + 'CONFIRM_PURCHASE_ORDER', + orderId, + { + orderNo: po.orderNo, + totalAmount: po.totalAmount, + lineCount: po.lines.length, + }, + companyId, + ); + + return updated; + } + + // ======================================= + // 删除草稿采购单 (仅 DRAFT) + // ======================================= + async delete(orderId: string, companyId: string, userId: string) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + if (po.status !== 'DRAFT') { + throw new BadRequestException('只能删除草稿状态的采购单'); + } + + await this.prisma.purchaseOrderLine.deleteMany({ where: { orderId } }); + await this.prisma.purchaseOrder.delete({ where: { id: orderId } }); + + await this.auditLog( + userId, + 'DELETE_PURCHASE_ORDER', + orderId, + { + orderNo: po.orderNo, + }, + companyId, + ); + + return { id: orderId, deleted: true }; + } + + // ======================================= + // 取消采购单 (DRAFT/SUBMITTED/APPROVED → CANCELLED) + // 已收货的采购单不允许直接取消 + // ======================================= + async cancel( + orderId: string, + companyId: string, + userId: string, + reason?: string, + ) { + const po = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + include: { lines: true, receipts: { where: { status: 'CONFIRMED' } } }, + }); + if (!po) throw new NotFoundException('采购单不存在或无权操作'); + + const cancellableStatuses: string[] = ['DRAFT', 'SUBMITTED', 'APPROVED']; + if (!cancellableStatuses.includes(po.status)) { + throw new BadRequestException( + `当前状态(${po.status})不允许取消,仅 DRAFT/SUBMITTED/APPROVED 状态可取消`, + ); + } + + if (po.receipts.length > 0) { + throw new BadRequestException( + '该采购单已有已确认的收货单,不允许直接取消。请先冲销相关收货单。', + ); + } + + const updated = await this.prisma.purchaseOrder.update({ + where: { id: orderId }, + data: { status: 'CANCELLED' }, + include: { lines: true, partner: true }, + }); + + await this.auditLog( + userId, + 'CANCEL_PURCHASE_ORDER', + orderId, + { + orderNo: po.orderNo, + previousStatus: po.status, + reason: reason ?? null, + }, + companyId, + ); + + this.logger.log(`采购单 ${po.orderNo} 已取消 (原状态: ${po.status})`); + return updated; + } +} diff --git a/apps/api/src/purchase/dto/purchase.dto.ts b/apps/api/src/purchase/dto/purchase.dto.ts new file mode 100644 index 0000000..0f2bd1f --- /dev/null +++ b/apps/api/src/purchase/dto/purchase.dto.ts @@ -0,0 +1,120 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsString, + IsNotEmpty, + IsNumber, + Min, + IsArray, + ValidateNested, + IsOptional, + IsEnum, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class PurchaseOrderItemDto { + @ApiProperty({ description: '物料ID' }) + @IsString() + @IsNotEmpty() + materialId!: string; + + @ApiProperty({ description: '采购数量' }) + @IsNumber() + @Min(1) + quantity!: number; + + @ApiProperty({ description: '单价' }) + @IsNumber() + @Min(0) + unitPrice!: number; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + note?: string; +} + +export class CreatePurchaseOrderDto { + @ApiProperty({ description: '供应商ID' }) + @IsString() + @IsNotEmpty() + supplierId!: string; + + @ApiProperty({ description: '采购单明细', type: [PurchaseOrderItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PurchaseOrderItemDto) + items!: PurchaseOrderItemDto[]; + + @ApiPropertyOptional({ description: '预计到货日期' }) + @IsOptional() + @IsString() + expectedDate?: string; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + notes?: string; +} + +export class UpdatePurchaseOrderStatusDto { + @ApiProperty({ + description: '新状态', + enum: ['APPROVED', 'CANCELLED'], + }) + @IsEnum(['APPROVED', 'CANCELLED']) + status!: string; +} + +export class ReceivePurchaseItemDto { + @ApiProperty({ description: '采购单ID' }) + @IsString() + @IsNotEmpty() + purchaseOrderId!: string; + + @ApiProperty({ description: '采购单明细ID' }) + @IsString() + @IsNotEmpty() + itemId!: string; + + @ApiProperty({ description: '本次收货数量' }) + @IsNumber() + @Min(0.01) + quantity!: number; + + @ApiProperty({ description: '目标库位ID' }) + @IsString() + @IsNotEmpty() + destLocationId!: string; + + @ApiPropertyOptional({ description: '批次号(支持扫描或手动输入)' }) + @IsOptional() + @IsString() + batchNo?: string; + + @ApiPropertyOptional({ description: '备注' }) + @IsOptional() + @IsString() + note?: string; +} + +export class ScanReceiveDto { + @ApiProperty({ description: '物料SKU码' }) + @IsString() + @IsNotEmpty() + materialSku!: string; + + @ApiProperty({ description: '收货数量' }) + @IsNumber() + @Min(0.01) + quantity!: number; + + @ApiProperty({ description: '目标库位ID' }) + @IsString() + @IsNotEmpty() + destLocationId!: string; + + @ApiPropertyOptional({ description: '批次号' }) + @IsOptional() + @IsString() + batchNo?: string; +} diff --git a/apps/api/src/purchase/purchase.controller.ts b/apps/api/src/purchase/purchase.controller.ts new file mode 100644 index 0000000..42f7f57 --- /dev/null +++ b/apps/api/src/purchase/purchase.controller.ts @@ -0,0 +1,118 @@ +import { + Controller, + Get, + Post, + Put, + Body, + Param, + UseGuards, + Query, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiQuery, +} from '@nestjs/swagger'; +import { PurchaseService } from './purchase.service'; +import { JwtAuthGuard } from '../core/guards/jwt-auth.guard'; +import { TenantGuard } from '../core/guards/tenant.guard'; +import { CurrentCompany } from '../core/decorators/current-company.decorator'; +import { CurrentUser } from '../core/decorators/current-user.decorator'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { + CreatePurchaseOrderDto, + ReceivePurchaseItemDto, + UpdatePurchaseOrderStatusDto, + ScanReceiveDto, +} from './dto/purchase.dto'; + +interface CurrentUserPayload { + id: string; +} + +@ApiTags('采购管理 (Purchase)') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, TenantGuard) +@Controller('purchase') +export class PurchaseController { + constructor(private readonly purchaseService: PurchaseService) {} + + @Post('orders') + @ApiOperation({ summary: '创建采购订单' }) + async createPurchaseOrder( + @CurrentCompany() companyId: string, + @Body() body: CreatePurchaseOrderDto, + ) { + return this.purchaseService.createPurchaseOrder(companyId, body); + } + + @Get('orders') + @ApiOperation({ summary: '获取采购订单列表' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'status', required: false, type: String }) + @ApiQuery({ name: 'search', required: false, type: String }) + async getPurchaseOrders( + @CurrentCompany() companyId: string, + @Query() pagination: PaginationDto, + @Query('status') status?: string, + @Query('search') search?: string, + ) { + return this.purchaseService.getPurchaseOrders( + companyId, + pagination, + status, + search, + ); + } + + @Get('orders/pending') + @ApiOperation({ summary: '获取待收货采购单列表' }) + async getPendingReceivableOrders(@CurrentCompany() companyId: string) { + return this.purchaseService.getPendingReceivableOrders(companyId); + } + + @Get('orders/:id') + @ApiOperation({ summary: '获取采购订单详情' }) + async getPurchaseOrderById( + @Param('id') orderId: string, + @CurrentCompany() companyId: string, + ) { + return this.purchaseService.getPurchaseOrderById(orderId, companyId); + } + + @Put('orders/:id/status') + @ApiOperation({ summary: '更新采购订单状态' }) + async updatePurchaseOrderStatus( + @Param('id') orderId: string, + @CurrentCompany() companyId: string, + @Body() body: UpdatePurchaseOrderStatusDto, + ) { + return this.purchaseService.updatePurchaseOrderStatus( + orderId, + companyId, + body, + ); + } + + @Post('orders/receive') + @ApiOperation({ summary: '采购收货执行' }) + async receivePurchaseItem( + @CurrentCompany() companyId: string, + @CurrentUser() user: CurrentUserPayload, + @Body() body: ReceivePurchaseItemDto, + ) { + return this.purchaseService.receivePurchaseItem(companyId, body, user.id); + } + + @Post('orders/scan-receive') + @ApiOperation({ summary: '扫码收货(自动匹配采购单)' }) + async scanReceive( + @CurrentCompany() companyId: string, + @CurrentUser() user: CurrentUserPayload, + @Body() body: ScanReceiveDto, + ) { + return this.purchaseService.scanReceive(companyId, body, user.id); + } +} diff --git a/apps/api/src/purchase/purchase.module.ts b/apps/api/src/purchase/purchase.module.ts new file mode 100644 index 0000000..0517930 --- /dev/null +++ b/apps/api/src/purchase/purchase.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PurchaseService } from './purchase.service'; +import { PurchaseController } from './purchase.controller'; +import { InventoryModule } from '../inventory/inventory.module'; + +@Module({ + imports: [InventoryModule], + providers: [PurchaseService], + controllers: [PurchaseController], + exports: [PurchaseService], +}) +export class PurchaseModule {} diff --git a/apps/api/src/purchase/purchase.service.spec.ts b/apps/api/src/purchase/purchase.service.spec.ts new file mode 100644 index 0000000..d8a9ef9 --- /dev/null +++ b/apps/api/src/purchase/purchase.service.spec.ts @@ -0,0 +1,128 @@ +import { BadRequestException } from '@nestjs/common'; +import { PurchaseOrderStatus } from '@prisma/client'; +import { PurchaseService } from './purchase.service'; + +type MockPrisma = { + partner: { findFirst: jest.Mock }; + material: { findMany: jest.Mock; findUnique: jest.Mock }; + purchaseOrder: { + create: jest.Mock; + findFirst: jest.Mock; + findMany: jest.Mock; + count: jest.Mock; + update: jest.Mock; + }; + stockLocation: { findFirst: jest.Mock }; + purchaseOrderLine: { findFirst: jest.Mock }; + $transaction: jest.Mock; +}; + +type MockTx = { + purchaseOrderLine: { update: jest.Mock; findMany: jest.Mock }; + purchaseOrder: { update: jest.Mock }; +}; + +describe('PurchaseService', () => { + const prisma: MockPrisma = { + partner: { findFirst: jest.fn() }, + material: { findMany: jest.fn(), findUnique: jest.fn() }, + purchaseOrder: { + create: jest.fn(), + findFirst: jest.fn(), + findMany: jest.fn(), + count: jest.fn(), + update: jest.fn(), + }, + stockLocation: { findFirst: jest.fn() }, + purchaseOrderLine: { findFirst: jest.fn() }, + $transaction: jest.fn(), + }; + const tx: MockTx = { + purchaseOrderLine: { update: jest.fn(), findMany: jest.fn() }, + purchaseOrder: { update: jest.fn() }, + }; + const inventoryService = { createStockMove: jest.fn() }; + let service: PurchaseService; + + beforeEach(() => { + jest.clearAllMocks(); + prisma.$transaction.mockImplementation( + (callback: (trx: MockTx) => unknown) => callback(tx), + ); + service = new PurchaseService( + prisma as unknown as ConstructorParameters[0], + inventoryService as unknown as ConstructorParameters< + typeof PurchaseService + >[1], + ); + }); + + it('creates legacy purchase orders as approved instead of confirmed', async () => { + prisma.partner.findFirst.mockResolvedValue({ id: 'supplier1' }); + prisma.material.findMany.mockResolvedValue([{ id: 'mat1' }]); + prisma.purchaseOrder.create.mockResolvedValue({ id: 'po1' }); + + await service.createPurchaseOrder('company1', { + supplierId: 'supplier1', + items: [{ materialId: 'mat1', quantity: 2, unitPrice: 5 }], + }); + + expect(prisma.purchaseOrder.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + status: PurchaseOrderStatus.APPROVED, + }), + }), + ); + }); + + it('blocks receiving purchase orders before approval', async () => { + prisma.purchaseOrder.findFirst.mockResolvedValue({ + id: 'po1', + orderNo: 'PO-001', + status: PurchaseOrderStatus.SUBMITTED, + lines: [ + { + id: 'line1', + materialId: 'mat1', + quantity: 5, + receivedQuantity: 0, + }, + ], + }); + + await expect( + service.receivePurchaseItem( + 'company1', + { + purchaseOrderId: 'po1', + itemId: 'line1', + quantity: 1, + destLocationId: 'loc1', + }, + 'user1', + ), + ).rejects.toBeInstanceOf(BadRequestException); + expect(inventoryService.createStockMove).not.toHaveBeenCalled(); + }); + + it('uses approved purchase orders for pending receivables', async () => { + prisma.purchaseOrder.findMany.mockResolvedValue([]); + + await service.getPendingReceivableOrders('company1'); + + expect(prisma.purchaseOrder.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + companyId: 'company1', + status: { + in: [ + PurchaseOrderStatus.APPROVED, + PurchaseOrderStatus.PARTIALLY_RECEIVED, + ], + }, + }, + }), + ); + }); +}); diff --git a/apps/api/src/purchase/purchase.service.ts b/apps/api/src/purchase/purchase.service.ts new file mode 100644 index 0000000..8c8bd10 --- /dev/null +++ b/apps/api/src/purchase/purchase.service.ts @@ -0,0 +1,344 @@ +import { + Injectable, + NotFoundException, + BadRequestException, +} from '@nestjs/common'; +import { Prisma, PurchaseOrderStatus } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { InventoryService } from '../inventory/inventory.service'; +import { PaginationDto } from '../core/dto/pagination.dto'; +import { + CreatePurchaseOrderDto, + ReceivePurchaseItemDto, + UpdatePurchaseOrderStatusDto, +} from './dto/purchase.dto'; + +@Injectable() +export class PurchaseService { + constructor( + private prisma: PrismaService, + private inventoryService: InventoryService, + ) {} + + /** + * 创建采购订单 + */ + async createPurchaseOrder(companyId: string, data: CreatePurchaseOrderDto) { + const supplier = await this.prisma.partner.findFirst({ + where: { + id: data.supplierId, + companyId, + type: { in: ['SUPPLIER', 'BOTH'] }, + }, + }); + if (!supplier) throw new NotFoundException('供应商不存在或无权限访问'); + + const materialIds = data.items.map((i) => i.materialId); + const materials = await this.prisma.material.findMany({ + where: { + id: { in: materialIds }, + OR: [{ companyId }, { companyId: null }], + }, + }); + if (materials.length !== materialIds.length) { + throw new BadRequestException('部分物料不存在'); + } + + let totalAmount = 0; + for (const item of data.items) + totalAmount += item.quantity * item.unitPrice; + + const now = new Date(); + const orderNo = `PO-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}-${Math.floor(1000 + Math.random() * 9000)}`; + + return this.prisma.purchaseOrder.create({ + data: { + orderNo, + partnerId: data.supplierId, + companyId, + status: PurchaseOrderStatus.APPROVED, + totalAmount, + notes: data.notes, + expectedDate: data.expectedDate ? new Date(data.expectedDate) : null, + lines: { + create: data.items.map((item) => ({ + materialId: item.materialId, + quantity: item.quantity, + unitPrice: item.unitPrice, + companyId, + description: item.note, + })), + }, + }, + include: { partner: true, lines: { include: { material: true } } }, + }); + } + + /** + * 获取采购订单列表 + */ + async getPurchaseOrders( + companyId: string, + pagination: PaginationDto, + status?: string, + search?: string, + ) { + const { page = 1, limit = 20 } = pagination; + const where: Prisma.PurchaseOrderWhereInput = { companyId }; + if (status && status in PurchaseOrderStatus) { + where.status = status as PurchaseOrderStatus; + } + if (search) { + where.OR = [ + { orderNo: { contains: search, mode: 'insensitive' } }, + { partner: { name: { contains: search, mode: 'insensitive' } } }, + ]; + } + + const [data, total] = await Promise.all([ + this.prisma.purchaseOrder.findMany({ + where, + include: { partner: true, lines: { include: { material: true } } }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + this.prisma.purchaseOrder.count({ where }), + ]); + + return { data, total, page, limit, totalPages: Math.ceil(total / limit) }; + } + + /** + * 获取采购订单详情(包含可收货数量) + */ + async getPurchaseOrderById(orderId: string, companyId: string) { + const order = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + include: { partner: true, lines: { include: { material: true } } }, + }); + if (!order) throw new NotFoundException('采购订单不存在或无权限访问'); + + const itemsWithRemaining = order.lines.map((item) => ({ + ...item, + remainingQty: Math.max(0, item.quantity - item.receivedQuantity), + })); + + return { ...order, items: itemsWithRemaining }; + } + + /** + * 更新采购订单状态 + */ + async updatePurchaseOrderStatus( + orderId: string, + companyId: string, + data: UpdatePurchaseOrderStatusDto, + ) { + const order = await this.prisma.purchaseOrder.findFirst({ + where: { id: orderId, companyId }, + }); + if (!order) throw new NotFoundException('采购订单不存在或无权限访问'); + if (order.status === 'CANCELLED') + throw new BadRequestException('已取消的采购单不能更新状态'); + if (order.status === 'RECEIVED' && data.status !== 'CANCELLED') { + throw new BadRequestException('已完成收货的采购单不能更改状态'); + } + + return this.prisma.purchaseOrder.update({ + where: { id: orderId }, + data: { status: data.status as PurchaseOrderStatus }, + include: { partner: true, lines: { include: { material: true } } }, + }); + } + + /** + * 采购收货执行 - 核心逻辑 + * 验证数量不超过可收货数量,调用库存服务创建入库记录 + */ + async receivePurchaseItem( + companyId: string, + data: ReceivePurchaseItemDto, + operatorId: string, + ) { + // 1. 验证采购单存在且状态正确 + const order = await this.prisma.purchaseOrder.findFirst({ + where: { id: data.purchaseOrderId, companyId }, + include: { lines: true }, + }); + if (!order) throw new NotFoundException('采购订单不存在或无权限访问'); + if (order.status === 'CANCELLED') + throw new BadRequestException('已取消的采购单不能收货'); + if (order.status === 'RECEIVED') + throw new BadRequestException('采购单已全部收货完成'); + const receivableStatuses: PurchaseOrderStatus[] = [ + PurchaseOrderStatus.APPROVED, + PurchaseOrderStatus.PARTIALLY_RECEIVED, + ]; + if (!receivableStatuses.includes(order.status)) { + throw new BadRequestException('只有已审批或部分收货的采购单才能收货'); + } + + // 2. 验证采购单明细存在 + const orderItem = order.lines.find((item) => item.id === data.itemId); + if (!orderItem) throw new NotFoundException('采购单明细不存在'); + + // 3. 验证收货数量不超过可收货数量 + const remainingQty = orderItem.quantity - orderItem.receivedQuantity; + if (remainingQty <= 0) + throw new BadRequestException('该明细已全部收货完成'); + if (data.quantity > remainingQty) { + throw new BadRequestException( + `收货数量(${data.quantity})超过可收货数量(${remainingQty})`, + ); + } + + // 4. 验证目标库位存在 + const location = await this.prisma.stockLocation.findFirst({ + where: { id: data.destLocationId, companyId }, + }); + if (!location) throw new NotFoundException('目标库位不存在或无权限访问'); + + // 5. 执行收货入库(在事务中完成) + const result = await this.prisma.$transaction(async (tx) => { + const referenceNo = `PURCHASE-RECV-${order.orderNo}`; + const inventoryTransaction = await this.inventoryService.createStockMove( + companyId, + { + materialId: orderItem.materialId!, + destLocationId: data.destLocationId, + quantity: data.quantity, + batchNo: data.batchNo, + referenceNo, + documentType: 'PURCHASE_RECEIPT', + documentId: order.id, + note: data.note ?? `采购收货入库:${order.orderNo}`, + }, + operatorId, + ); + + const newReceivedQty = orderItem.receivedQuantity + data.quantity; + await tx.purchaseOrderLine.update({ + where: { id: orderItem.id }, + data: { receivedQuantity: newReceivedQty }, + }); + + const allItems = await tx.purchaseOrderLine.findMany({ + where: { orderId: order.id }, + }); + const allReceived = allItems.every( + (item) => item.receivedQuantity >= item.quantity, + ); + const anyReceived = allItems.some((item) => item.receivedQuantity > 0); + + let newStatus = order.status; + if (allReceived) newStatus = PurchaseOrderStatus.RECEIVED; + else if (anyReceived) newStatus = PurchaseOrderStatus.PARTIALLY_RECEIVED; + + await tx.purchaseOrder.update({ + where: { id: order.id }, + data: { status: newStatus }, + }); + + return { + transactionId: inventoryTransaction.id, + purchaseOrderNo: order.orderNo, + receivedQuantity: data.quantity, + newReceivedQty, + remainingQty: orderItem.quantity - newReceivedQty, + newOrderStatus: newStatus, + message: allReceived + ? '收货完成,采购单已全部收货' + : `收货成功,剩余可收货数量:${orderItem.quantity - newReceivedQty}`, + }; + }); + + return result; + } + + /** + * 扫码收货 - 通过物料条码自动匹配最近的采购单明细 + */ + async scanReceive( + companyId: string, + data: { + materialSku: string; + quantity: number; + destLocationId: string; + batchNo?: string; + }, + operatorId: string, + ) { + const material = await this.prisma.material.findUnique({ + where: { sku: data.materialSku }, + }); + if (!material) throw new NotFoundException('未找到对应条码的物料'); + + const orderItem = await this.prisma.purchaseOrderLine.findFirst({ + where: { + materialId: material.id, + order: { + companyId, + status: { + in: [ + PurchaseOrderStatus.APPROVED, + PurchaseOrderStatus.PARTIALLY_RECEIVED, + ], + }, + }, + }, + include: { order: true }, + orderBy: { order: { createdAt: 'desc' } }, + }); + if (!orderItem) throw new BadRequestException('未找到该物料的待收货采购单'); + + const remainingQty = orderItem.quantity - orderItem.receivedQuantity; + if (remainingQty <= 0) + throw new BadRequestException('该采购明细已全部收货'); + + const receiveQty = Math.min(data.quantity, remainingQty); + + return this.receivePurchaseItem( + companyId, + { + purchaseOrderId: orderItem.orderId, + itemId: orderItem.id, + quantity: receiveQty, + destLocationId: data.destLocationId, + batchNo: data.batchNo, + }, + operatorId, + ); + } + + /** + * 获取待收货的采购单列表(供前端选择) + */ + async getPendingReceivableOrders(companyId: string) { + const orders = await this.prisma.purchaseOrder.findMany({ + where: { + companyId, + status: { + in: [ + PurchaseOrderStatus.APPROVED, + PurchaseOrderStatus.PARTIALLY_RECEIVED, + ], + }, + }, + include: { + partner: true, + lines: { include: { material: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + + return orders + .map((order) => { + const items = order.lines.filter( + (item) => item.receivedQuantity < item.quantity, + ); + return { ...order, items }; + }) + .filter((order) => order.items.length > 0); + } +} diff --git a/apps/api/test/migrations/migration.e2e-spec.ts b/apps/api/test/migrations/migration.e2e-spec.ts new file mode 100644 index 0000000..6da5ee0 --- /dev/null +++ b/apps/api/test/migrations/migration.e2e-spec.ts @@ -0,0 +1,673 @@ +/** + * Prisma 迁移集成测试 + * + * 三个核心场景: + * 1. 空库 migrate deploy — 全量迁移后验证完整 schema + * 2. 旧库迁移到当前 schema — 逐步迁移 + 增量数据验证 + * 3. TaxCode 历史数据策略 — 迁移后历史行的默认值断言 + * + * 运行前提: + * - 环境变量 DATABASE_URL 指向测试库 (会 DROP ALL TABLES) + * - PostgreSQL >= 14 + * + * 运行方式: + * DATABASE_URL=postgresql://... npx jest --config ./test/jest-e2e.json test/migrations/migration.e2e-spec.ts + */ + +import { Client } from 'pg'; +import { readFileSync, readdirSync } from 'fs'; +import { join } from 'path'; +import { execSync } from 'child_process'; + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +const PRISMA_DIR = join(__dirname, '../../prisma'); +const MIGRATIONS_DIR = join(PRISMA_DIR, 'migrations'); + +/** 按文件名排序返回所有 migration 目录 */ +function getMigrationDirs(): string[] { + return readdirSync(MIGRATIONS_DIR, { withFileTypes: true }) + .filter((d) => d.isDirectory() && /^\d{14}_/.test(d.name)) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((d) => d.name); +} + +/** 读取单个 migration.sql 内容 */ +function readMigrationSql(dirName: string): string { + const sqlPath = join(MIGRATIONS_DIR, dirName, 'migration.sql'); + return readFileSync(sqlPath, 'utf-8'); +} + +/** 安全执行 SQL,忽略 "already exists" 类错误 */ +async function safeExec(client: Client, sql: string): Promise { + try { + await client.query(sql); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if (/already exists|duplicate/i.test(msg)) return; + throw err; + } +} + +/** 删除所有 public 表 (测试前清库) */ +async function dropAllTables(client: Client): Promise { + await client.query(` + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP + EXECUTE 'DROP TABLE IF EXISTS "' || r.tablename || '" CASCADE'; + END LOOP; + END $$; + `); + await client.query('DROP TABLE IF EXISTS "_prisma_migrations" CASCADE'); +} + +/** 获取指定表的所有列名 */ +async function getColumns( + client: Client, + tableName: string, +): Promise> { + const res = await client.query( + `SELECT column_name FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1`, + [tableName], + ); + return new Set(res.rows.map((r: { column_name: string }) => r.column_name)); +} + +/** 检查表是否存在 */ +async function tableExists( + client: Client, + tableName: string, +): Promise { + const res = await client.query( + `SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = $1 + ) AS exists`, + [tableName], + ); + return res.rows[0].exists as boolean; +} + +// --------------------------------------------------------------------------- +// 按 Prisma schema 期望的完整表清单 +// --------------------------------------------------------------------------- +const EXPECTED_TABLES = [ + 'Company', + 'User', + 'Role', + 'UserCompanyRole', + 'Department', + 'Partner', + 'TaxCode', + 'Order', + 'OrderItem', + 'Warehouse', + 'StockLocation', + 'Material', + 'ProductCategory', + 'Product', + 'Bom', + 'BomLine', + 'StockQuant', + 'FileRecord', + 'InventoryTransaction', + 'Workflow', + 'WorkflowState', + 'WorkflowTransition', + 'WorkOrder', + 'WorkReport', + 'Invoice', + 'Payment', + 'Account', + 'Journal', + 'JournalEntry', + 'JournalEntryLine', + 'EventDlq', + 'AuditLog', +]; + +// --------------------------------------------------------------------------- +// tests +// --------------------------------------------------------------------------- + +describe('Prisma 迁移集成测试', () => { + let client: Client; + const databaseUrl = process.env.DATABASE_URL; + + beforeAll(async () => { + if (!databaseUrl) { + throw new Error( + 'DATABASE_URL 环境变量未设置。请指向测试 PostgreSQL 数据库。', + ); + } + client = new Client({ connectionString: databaseUrl }); + await client.connect(); + }); + + afterAll(async () => { + await client?.end(); + }); + + // ========================================================================= + // 场景 1: 空库 migrate deploy + // ========================================================================= + describe('场景 1: 空库 migrate deploy', () => { + beforeAll(async () => { + await dropAllTables(client); + }); + + it('应当通过 prisma migrate deploy 完成全量迁移', () => { + expect(() => { + execSync('npx prisma migrate deploy', { + cwd: join(__dirname, '../..'), + env: { ...process.env, DATABASE_URL: databaseUrl }, + stdio: 'pipe', + timeout: 60_000, + }); + }).not.toThrow(); + }); + + it('所有预期表应当存在', async () => { + for (const table of EXPECTED_TABLES) { + const exists = await tableExists(client, table); + expect(exists).toBe(true); + } + }); + + it('TaxCode 表应当包含全部字段', async () => { + const cols = await getColumns(client, 'TaxCode'); + const expectedCols = [ + 'id', + 'code', + 'name', + 'rate', + 'isTaxInclusive', + 'isDefault', + 'active', + 'accountId', + 'companyId', + 'createdAt', + 'updatedAt', + ]; + for (const col of expectedCols) { + expect(cols.has(col)).toBe(true); + } + }); + + it('Order 表应当包含税码快照字段', async () => { + const cols = await getColumns(client, 'Order'); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxTotal')).toBe(true); + expect(cols.has('taxCodeId')).toBe(true); + }); + + it('OrderItem 表应当包含税码快照字段', async () => { + const cols = await getColumns(client, 'OrderItem'); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxAmount')).toBe(true); + expect(cols.has('taxRate')).toBe(true); + expect(cols.has('taxCodeId')).toBe(true); + }); + + it('Invoice 表应当包含税码快照字段', async () => { + const cols = await getColumns(client, 'Invoice'); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxAmount')).toBe(true); + expect(cols.has('taxCodeId')).toBe(true); + }); + + it('StockLocation 表应当包含 parentId 自引用字段', async () => { + const cols = await getColumns(client, 'StockLocation'); + expect(cols.has('parentId')).toBe(true); + }); + + it('Account 表应当存在(TaxCode 关联依赖)', async () => { + const cols = await getColumns(client, 'Account'); + expect(cols.has('id')).toBe(true); + expect(cols.has('code')).toBe(true); + expect(cols.has('companyId')).toBe(true); + }); + + it('唯一索引 TaxCode(companyId, code) 应当存在', async () => { + const res = await client.query(` + SELECT indexname FROM pg_indexes + WHERE tablename = 'TaxCode' + AND indexname = 'TaxCode_companyId_code_key' + `); + expect(res.rowCount).toBe(1); + }); + + it('外键约束 Order → TaxCode 应当存在', async () => { + const res = await client.query(` + SELECT conname FROM pg_constraint + WHERE conname = 'Order_taxCodeId_fkey' + `); + expect(res.rowCount).toBe(1); + }); + }); + + // ========================================================================= + // 场景 2: 旧库迁移到当前 schema + // ========================================================================= + describe('场景 2: 旧库迁移到当前 schema', () => { + const allDirs = getMigrationDirs(); + const taxcodeIdx = allDirs.findIndex((d) => d.includes('taxcode_engine')); + const preTaxDirs = allDirs.slice(0, taxcodeIdx); + const taxcodeDir = allDirs[taxcodeIdx]; + + beforeAll(async () => { + await dropAllTables(client); + }); + + it('应当能逐步执行 TaxCode 迁移之前的所有脚本', async () => { + for (const dirName of preTaxDirs) { + const sql = readMigrationSql(dirName); + if (sql.trim() === '-- no-op after baseline rewrite') continue; + await safeExec(client, sql); + } + expect(await tableExists(client, 'Company')).toBe(true); + expect(await tableExists(client, 'Order')).toBe(true); + expect(await tableExists(client, 'OrderItem')).toBe(true); + expect(await tableExists(client, 'Invoice')).toBe(true); + }); + + it('迁移前 Order 表不应有 taxCodeId 列', async () => { + if (taxcodeIdx > 0) { + const cols = await getColumns(client, 'Order'); + expect(cols.has('taxCodeId')).toBe(false); + } + }); + + it('迁移前不应存在 TaxCode 表', async () => { + expect(await tableExists(client, 'TaxCode')).toBe(false); + }); + + it('应当能执行 TaxCode 迁移脚本', async () => { + const sql = readMigrationSql(taxcodeDir); + await safeExec(client, sql); + expect(await tableExists(client, 'TaxCode')).toBe(true); + }); + + it('迁移后 Order 表应当出现 taxCodeId 列', async () => { + const cols = await getColumns(client, 'Order'); + expect(cols.has('taxCodeId')).toBe(true); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxTotal')).toBe(true); + }); + + it('迁移后 OrderItem 表应当出现税码字段', async () => { + const cols = await getColumns(client, 'OrderItem'); + expect(cols.has('taxCodeId')).toBe(true); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxAmount')).toBe(true); + expect(cols.has('taxRate')).toBe(true); + }); + + it('迁移后 Invoice 表应当出现税码字段', async () => { + const cols = await getColumns(client, 'Invoice'); + expect(cols.has('taxCodeId')).toBe(true); + expect(cols.has('subTotal')).toBe(true); + expect(cols.has('taxAmount')).toBe(true); + }); + + it('应当能执行剩余所有迁移脚本', async () => { + const remainingDirs = allDirs.slice(taxcodeIdx + 1); + for (const dirName of remainingDirs) { + const sql = readMigrationSql(dirName); + if (sql.trim() === '-- no-op after baseline rewrite') continue; + await safeExec(client, sql); + } + const cols = await getColumns(client, 'StockLocation'); + expect(cols.has('parentId')).toBe(true); + }); + }); + + // ========================================================================= + // 场景 3: TaxCode 历史数据策略验证 + // ========================================================================= + describe('场景 3: TaxCode 历史数据策略验证', () => { + let companyId: string; + let userId: string; + let partnerId: string; + + beforeAll(async () => { + await dropAllTables(client); + execSync('npx prisma migrate deploy', { + cwd: join(__dirname, '../..'), + env: { ...process.env, DATABASE_URL: databaseUrl }, + stdio: 'pipe', + timeout: 60_000, + }); + }); + + it('应当能插入 Company 基础数据', async () => { + const res = await client.query(` + INSERT INTO "Company" ("id", "name", "updatedAt") + VALUES (gen_random_uuid(), '测试公司', NOW()) + RETURNING "id" + `); + companyId = res.rows[0].id; + expect(companyId).toBeTruthy(); + }); + + it('应当能插入 User 基础数据', async () => { + const res = await client.query(` + INSERT INTO "User" ("id", "email", "passwordHash", "name", "updatedAt") + VALUES (gen_random_uuid(), 'test@example.com', 'hash', '测试用户', NOW()) + RETURNING "id" + `); + userId = res.rows[0].id; + expect(userId).toBeTruthy(); + }); + + it('应当能插入 Partner 基础数据', async () => { + const res = await client.query( + ` + INSERT INTO "Partner" ("id", "name", "companyId", "updatedAt") + VALUES (gen_random_uuid(), '测试客户', $1, NOW()) + RETURNING "id" + `, + [companyId], + ); + partnerId = res.rows[0].id; + expect(partnerId).toBeTruthy(); + }); + + it('应当能创建 TaxCode 主数据', async () => { + const res = await client.query( + ` + INSERT INTO "TaxCode" ("id", "code", "name", "rate", "companyId", "updatedAt") + VALUES (gen_random_uuid(), 'VAT_13', '增值税 13%', 0.13, $1, NOW()) + RETURNING "id", "code", "rate" + `, + [companyId], + ); + expect(res.rows[0].code).toBe('VAT_13'); + expect(Number(res.rows[0].rate)).toBeCloseTo(0.13); + }); + + describe('历史订单的默认税码快照策略', () => { + let orderId: string; + + it('应当能创建不含 taxCodeId 的历史订单(模拟旧数据)', async () => { + const res = await client.query( + ` + INSERT INTO "Order" ("id", "orderNo", "partnerId", "salesId", "companyId", "status", "totalAmount", "updatedAt") + VALUES (gen_random_uuid(), 'ORD-HIST-001', $1, $2, $3, 'DRAFT', 1000, NOW()) + RETURNING "id" + `, + [partnerId, userId, companyId], + ); + orderId = res.rows[0].id; + + const row = await client.query( + ` + SELECT "subTotal", "taxTotal", "taxCodeId" + FROM "Order" WHERE "id" = $1 + `, + [orderId], + ); + + expect(Number(row.rows[0].subTotal)).toBe(0); + expect(Number(row.rows[0].taxTotal)).toBe(0); + expect(row.rows[0].taxCodeId).toBeNull(); + }); + + it('应当能创建不含 taxCodeId 的历史订单行', async () => { + await client.query( + ` + INSERT INTO "OrderItem" ("id", "orderId", "productId", "quantity", "unitPrice", "totalPrice", "updatedAt") + VALUES (gen_random_uuid(), $1, 'PROD-001', 10, 100, 1000, NOW()) + `, + [orderId], + ); + + const row = await client.query( + ` + SELECT "subTotal", "taxAmount", "taxRate", "taxCodeId" + FROM "OrderItem" WHERE "orderId" = $1 + `, + [orderId], + ); + + expect(Number(row.rows[0].subTotal)).toBe(0); + expect(Number(row.rows[0].taxAmount)).toBe(0); + expect(Number(row.rows[0].taxRate)).toBe(0); + expect(row.rows[0].taxCodeId).toBeNull(); + }); + + it('历史订单应当可查询且 tax 字段全为零', async () => { + const res = await client.query(` + SELECT "id", "subTotal", "taxTotal", "taxCodeId" + FROM "Order" WHERE "orderNo" = 'ORD-HIST-001' + `); + expect(res.rowCount).toBe(1); + expect(Number(res.rows[0].subTotal)).toBe(0); + expect(Number(res.rows[0].taxTotal)).toBe(0); + expect(res.rows[0].taxCodeId).toBeNull(); + }); + }); + + describe('含税码的新订单快照策略', () => { + let taxCodeId: string; + + it('应当能查询已创建的 TaxCode', async () => { + const res = await client.query( + ` + SELECT "id" FROM "TaxCode" WHERE "code" = 'VAT_13' AND "companyId" = $1 + `, + [companyId], + ); + expect(res.rowCount).toBe(1); + taxCodeId = res.rows[0].id; + }); + + it('新订单应能关联 taxCodeId 并记录快照', async () => { + await client.query( + ` + INSERT INTO "Order" ( + "id", "orderNo", "partnerId", "salesId", "companyId", + "status", "totalAmount", "subTotal", "taxTotal", "taxCodeId", "updatedAt" + ) VALUES ( + gen_random_uuid(), 'ORD-NEW-001', $1, $2, $3, + 'DRAFT', 1130, 1000, 130, $4, NOW() + ) + `, + [partnerId, userId, companyId, taxCodeId], + ); + + const row = await client.query(` + SELECT "subTotal", "taxTotal", "taxCodeId" + FROM "Order" WHERE "orderNo" = 'ORD-NEW-001' + `); + + expect(Number(row.rows[0].subTotal)).toBe(1000); + expect(Number(row.rows[0].taxTotal)).toBe(130); + expect(row.rows[0].taxCodeId).toBe(taxCodeId); + }); + + it('新订单行应能关联 taxCodeId 并记录税率', async () => { + const orderRes = await client.query(` + SELECT "id" FROM "Order" WHERE "orderNo" = 'ORD-NEW-001' + `); + const orderId = orderRes.rows[0].id; + + await client.query( + ` + INSERT INTO "OrderItem" ( + "id", "orderId", "productId", "quantity", "unitPrice", "totalPrice", + "subTotal", "taxAmount", "taxRate", "taxCodeId", "updatedAt" + ) VALUES ( + gen_random_uuid(), $1, 'PROD-002', 10, 100, 1130, + 1000, 130, 0.13, $2, NOW() + ) + `, + [orderId, taxCodeId], + ); + + const row = await client.query( + ` + SELECT "subTotal", "taxAmount", "taxRate", "taxCodeId" + FROM "OrderItem" WHERE "orderId" = $1 + `, + [orderId], + ); + + expect(Number(row.rows[0].subTotal)).toBe(1000); + expect(Number(row.rows[0].taxAmount)).toBe(130); + expect(Number(row.rows[0].taxRate)).toBeCloseTo(0.13); + expect(row.rows[0].taxCodeId).toBe(taxCodeId); + }); + }); + + describe('Invoice 的税码快照策略', () => { + it('历史 Invoice(无 taxCodeId)应当默认值为零', async () => { + const orderRes = await client.query(` + SELECT "id" FROM "Order" WHERE "orderNo" = 'ORD-HIST-001' + `); + const orderId = orderRes.rows[0].id; + + await client.query( + ` + INSERT INTO "Invoice" ( + "id", "invoiceNo", "orderId", "amount", "status", "companyId", "updatedAt" + ) VALUES ( + gen_random_uuid(), 'INV-HIST-001', $1, 1000, 'UNPAID', $2, NOW() + ) + `, + [orderId, companyId], + ); + + const row = await client.query(` + SELECT "subTotal", "taxAmount", "taxCodeId" + FROM "Invoice" WHERE "invoiceNo" = 'INV-HIST-001' + `); + + expect(Number(row.rows[0].subTotal)).toBe(0); + expect(Number(row.rows[0].taxAmount)).toBe(0); + expect(row.rows[0].taxCodeId).toBeNull(); + }); + + it('新 Invoice 可关联 TaxCode 并记录税额快照', async () => { + const orderRes = await client.query(` + SELECT "id" FROM "Order" WHERE "orderNo" = 'ORD-NEW-001' + `); + const taxCodeRes = await client.query( + ` + SELECT "id" FROM "TaxCode" WHERE "code" = 'VAT_13' AND "companyId" = $1 + `, + [companyId], + ); + + await client.query( + ` + INSERT INTO "Invoice" ( + "id", "invoiceNo", "orderId", "amount", + "subTotal", "taxAmount", "taxCodeId", + "status", "companyId", "postingStatus", "updatedAt" + ) VALUES ( + gen_random_uuid(), 'INV-NEW-001', $1, 1130, + 1000, 130, $2, + 'UNPAID', $3, 'DRAFT', NOW() + ) + `, + [orderRes.rows[0].id, taxCodeRes.rows[0].id, companyId], + ); + + const row = await client.query(` + SELECT "subTotal", "taxAmount", "taxCodeId" + FROM "Invoice" WHERE "invoiceNo" = 'INV-NEW-001' + `); + + expect(Number(row.rows[0].subTotal)).toBe(1000); + expect(Number(row.rows[0].taxAmount)).toBe(130); + expect(row.rows[0].taxCodeId).toBe(taxCodeRes.rows[0].id); + }); + }); + + describe('TaxCode 约束与索引验证', () => { + it('同一公司下不允许重复 code', async () => { + await expect( + client.query( + ` + INSERT INTO "TaxCode" ("id", "code", "name", "rate", "companyId", "updatedAt") + VALUES (gen_random_uuid(), 'VAT_13', '重复税码', 0.13, $1, NOW()) + `, + [companyId], + ), + ).rejects.toThrow(/unique/i); + }); + + it('不同公司可以有相同 code', async () => { + const res = await client.query(` + INSERT INTO "Company" ("id", "name", "updatedAt") + VALUES (gen_random_uuid(), '第二公司', NOW()) + RETURNING "id" + `); + const company2Id = res.rows[0].id; + + await client.query( + ` + INSERT INTO "TaxCode" ("id", "code", "name", "rate", "companyId", "updatedAt") + VALUES (gen_random_uuid(), 'VAT_13', '增值税 13%', 0.13, $1, NOW()) + `, + [company2Id], + ); + + const count = await client.query(` + SELECT COUNT(*)::int AS cnt FROM "TaxCode" WHERE "code" = 'VAT_13' + `); + expect(count.rows[0].cnt).toBe(2); + }); + + it('TaxCode 的 companyId 外键应引用 Company', async () => { + await expect( + client.query(` + INSERT INTO "TaxCode" ("id", "code", "name", "rate", "companyId", "updatedAt") + VALUES (gen_random_uuid(), 'INVALID', '无效', 0, 'non-existent-id', NOW()) + `), + ).rejects.toThrow(/violates foreign key/i); + }); + + it('taxCodeId 外键 SET NULL: 删除 TaxCode 不应级联删除关联订单', async () => { + const orderRes = await client.query(` + SELECT o."id" AS "orderId", tc."id" AS "taxCodeId" + FROM "Order" o + JOIN "TaxCode" tc ON o."taxCodeId" = tc."id" + WHERE tc."code" = 'VAT_13' + LIMIT 1 + `); + + if (orderRes.rowCount === 0) return; + + const { orderId, taxCodeId } = orderRes.rows[0]; + await client.query(`DELETE FROM "TaxCode" WHERE "id" = $1`, [ + taxCodeId, + ]); + + const checkRes = await client.query( + `SELECT "taxCodeId" FROM "Order" WHERE "id" = $1`, + [orderId], + ); + expect(checkRes.rowCount).toBe(1); + expect(checkRes.rows[0].taxCodeId).toBeNull(); + }); + }); + + describe('批量迁移幂等性验证', () => { + it('重复执行 taxcode_engine 迁移 SQL 不应报错', async () => { + const dirs = getMigrationDirs(); + const taxcodeDir = dirs.find((d) => d.includes('taxcode_engine')); + expect(taxcodeDir).toBeDefined(); + + const sql = readMigrationSql(taxcodeDir!); + await expect(safeExec(client, sql)).resolves.not.toThrow(); + }); + }); + }); +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index b31d88b..797df74 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "5.0", "module": "nodenext", "moduleResolution": "nodenext", "resolvePackageJsonExports": true, diff --git a/apps/desktop/tauri.conf.json b/apps/desktop/tauri.conf.json index b7711c5..94d7995 100644 --- a/apps/desktop/tauri.conf.json +++ b/apps/desktop/tauri.conf.json @@ -58,7 +58,7 @@ "fullscreen": false, "height": 900, "resizable": true, - "title": "智能制造 EIP 全局系统", + "title": "OneERP Operations Platform", "width": 1400, "decorations": false, "transparent": true diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index fd403d5..65baa8c 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -138,7 +138,7 @@ export default function App() { setShowSettings(true)} style={{ position: 'absolute', right: 0, top: 0, padding: 10 }}> - EIP 扫码终端 + OneERP 扫码终端 仓库作业专用移动设备 =6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -238,6 +285,245 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -295,6 +581,128 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -1007,1100 +1415,1009 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.1.tgz", - "integrity": "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { - "fast-glob": "3.3.1" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", - "cpu": [ - "x64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", - "cpu": [ - "x64" - ], + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, "engines": { - "node": ">= 10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", - "cpu": [ - "x64" - ], + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, "engines": { - "node": ">= 10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@jest/core/node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">= 10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" }, "engines": { - "node": ">= 8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", - "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", - "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + "canvas": "^3.0.0", + "jsdom": "*" }, "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { + "canvas": { "optional": true } } }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", - "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.8.0" + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.2" + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", - "cpu": [ - "x64" - ], + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", - "cpu": [ - "x64" - ], + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", - "cpu": [ - "arm" - ], + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", - "cpu": [ - "x64" - ], + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", - "cpu": [ - "x64" - ], + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, "engines": { - "node": ">= 20" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=14.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", - "cpu": [ - "arm64" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", - "cpu": [ - "x64" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 20" + "node": ">=6.0.0" } }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "postcss": "^8.5.6", - "tailwindcss": "4.2.2" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.23", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", - "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "node_modules/@next/env": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", + "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.1.tgz", + "integrity": "sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.23" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "fast-glob": "3.3.1" } }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", + "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "node": ">= 10" } }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.23", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", - "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", + "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "cpu": [ + "x64" + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", + "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "cpu": [ + "arm64" + ], "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", + "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@types/d3-color": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", + "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@types/d3-time": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", + "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@types/d3-path": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", + "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "dev": true, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", + "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "engines": { + "node": ">= 8" } }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">= 8" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=12.4.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3" - }, + "optional": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=14" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/pkgr" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", - "dev": true, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", - "dev": true, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/immer" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } + "license": "MIT" }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "tslib": "^2.8.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">= 20" }, - "funding": { - "url": "https://opencollective.com/eslint" + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -2109,12 +2426,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -2123,12 +2443,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -2137,12 +2460,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -2151,12 +2477,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -2165,26 +2494,32 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -2193,127 +2528,143 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "riscv64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.8.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.8.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", "dev": true, + "inBundle": true, "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" + "tslib": "^2.4.0" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -2322,261 +2673,2946 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.1.tgz", + "integrity": "sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.1", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "ms": "^2.1.1" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "color-convert": "^2.0.1" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" }, "engines": { - "node": ">=8" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" + "debug": "^3.2.7" }, "engines": { - "node": ">= 0.4" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ms": "^2.1.1" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "ms": "^2.1.1" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=4.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "engines": { "node": ">= 0.4" @@ -2585,478 +5621,491 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "url": "https://opencollective.com/eslint" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", - "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "fill-range": "^7.1.1" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, + "license": "BSD-2-Clause", "bin": { - "browserslist": "cli.js" + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=4" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": ">=7.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">= 0.8" + "node": ">=8.6.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "reusify": "^1.0.4" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "license": "MIT", + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "tiny-invariant": "^1.0.6" + "bser": "2.1.1" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", "dependencies": { - "internmap": "1 - 2" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=16.0.0" } }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, "engines": { - "node": ">=12" + "node": ">=16" } }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { - "d3-path": "^3.1.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=12" + "node": ">= 6" } }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-time": "1 - 3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3065,34 +6114,52 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/inspect-js" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3101,65 +6168,102 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "resolve-pkg-maps": "^1.0.0" }, - "engines": { - "node": ">=6.0" + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -3168,141 +6272,107 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", "license": "MIT", - "engines": { - "node": ">=0.4.0" + "peerDependencies": { + "csstype": "^3.0.10" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "Apache-2.0", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" + "dunder-proto": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3311,473 +6381,373 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-iterator-helpers": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", - "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0", - "safe-array-concat": "^1.1.3" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.4" + "node": ">= 14" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">= 0.4" + "node": ">= 14" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/es-toolkit": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", - "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 4" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" }, "bin": { - "eslint": "bin/eslint.js" + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8" }, "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-config-next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.1.tgz", - "integrity": "sha512-qhabwjQZ1Mk53XzXvmogf8KQ0tG0CQXF0CZ56+2/lVhmObgmaqj7x5A1DSrWdZd3kwI7GTPGUjFne+krRxYmFg==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "16.2.1", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=0.8.19" } }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } + "node": ">=12" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": ">=4" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } + "license": "MIT" }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=4" + "node": ">= 0.4" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "node": ">=10" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, "engines": { "node": ">= 0.4" }, @@ -3785,259 +6755,236 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "hasown": "^2.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.12.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=16" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4046,44 +6993,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", + "node": ">=8" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4092,53 +7023,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, "engines": { "node": ">= 0.4" }, @@ -4146,28 +7070,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", - "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" }, "engines": { @@ -4177,714 +7103,1115 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">=10" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "is-glob": "^4.0.3" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=10.13.0" + "node": ">=10" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/goober": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", - "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, "peerDependencies": { - "csstype": "^3.0.10" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.0" + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" + "node_modules/jest-cli/node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/jest-cli/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "node_modules/jest-cli/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, "license": "MIT", "dependencies": { - "hermes-estree": "0.25.1" + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", - "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", - "license": "MIT", + "node": ">=10" + }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, "engines": { - "node": ">=0.8.19" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/jest-each/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", "dev": true, "license": "MIT", "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.2" + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "dependencies": { - "semver": "^7.7.1" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">=10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, "engines": { - "node": ">=0.12.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=10" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.16" + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/jiti": { @@ -4917,6 +8244,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4937,6 +8304,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5007,7 +8381,17 @@ "language-subtag-registry": "^0.3.20" }, "engines": { - "node": ">=0.10" + "node": ">=0.10" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/levn": { @@ -5285,6 +8669,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5301,6 +8692,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5340,6 +8738,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5350,6 +8759,52 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5359,6 +8814,13 @@ "node": ">= 0.4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5404,6 +8866,26 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5427,6 +8909,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5475,6 +8967,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/next": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", @@ -5575,6 +9074,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -5582,6 +9088,36 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5705,6 +9241,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5773,6 +9335,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5786,6 +9365,38 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5796,6 +9407,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5813,6 +9434,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5820,9 +9465,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5832,6 +9477,85 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5843,9 +9567,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -5881,6 +9605,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5894,10 +9656,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -5909,6 +9674,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6033,6 +9815,20 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -6092,6 +9888,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -6119,6 +9925,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6150,6 +9979,13 @@ "node": ">=0.10.0" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6229,6 +10065,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6451,6 +10307,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6460,6 +10349,24 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6467,6 +10374,29 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6481,6 +10411,87 @@ "node": ">= 0.4" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6594,6 +10605,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6604,6 +10658,29 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6666,6 +10743,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -6697,6 +10797,43 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -6739,9 +10876,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6751,6 +10888,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6758,10 +10922,36 @@ "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" }, "engines": { - "node": ">=8.0" + "node": ">=18" } }, "node_modules/ts-api-utils": { @@ -6777,6 +10967,85 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -6822,6 +11091,29 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6938,6 +11230,20 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -7049,6 +11355,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -7071,6 +11392,77 @@ "d3-timer": "^3.0.1" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7186,6 +11578,168 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7193,6 +11747,70 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 8a43eb7..f514eab 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,13 +41,14 @@ "identity-obj-proxy": "^3.0.0", "jest": "^30.0.0", "jest-environment-jsdom": "^30.0.0", + "jsdom": "^26.1.0", "tailwindcss": "^4", "ts-jest": "^29.2.5", "typescript": "^5" }, "jest": { "testEnvironment": "jsdom", - "setupFilesAfterSetup": [ + "setupFilesAfterEnv": [ "/jest.setup.ts" ], "moduleNameMapper": { @@ -66,6 +67,9 @@ "testPathIgnorePatterns": [ "/.next/", "/node_modules/" + ], + "modulePathIgnorePatterns": [ + "/.next/" ] } } diff --git a/apps/web/src/__tests__/web-smoke.test.tsx b/apps/web/src/__tests__/web-smoke.test.tsx new file mode 100644 index 0000000..be7f62c --- /dev/null +++ b/apps/web/src/__tests__/web-smoke.test.tsx @@ -0,0 +1,424 @@ +/** + * Web 冒烟测试 — 核心前端功能可用性验证 + * + * 覆盖: + * 1. 登录页:渲染 + 默认值 + 提交 + 错误 + * 2. DynamicView:列表加载 / 新建 / 编辑 / 引用字段选择 + * 3. 订单详情 timeline:事件渲染 / 空态 / 批注提交 + * + * 这是一个聚合层面的冒烟测试,验证各核心页面/组件的关键路径。 + */ + +/* ======================================================== + Mock 公共依赖 + ======================================================== */ + +/* localStorage */ +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: jest.fn((k: string) => store[k] ?? null), + setItem: jest.fn((k: string, v: string) => { store[k] = v; }), + removeItem: jest.fn((k: string) => { delete store[k]; }), + clear: jest.fn(() => { store = {}; }), + get length() { return Object.keys(store).length; }, + key: jest.fn((i: number) => Object.keys(store)[i] ?? null), + }; +})(); +Object.defineProperty(global, 'localStorage', { value: localStorageMock }); + +/* next/navigation */ +const mockRouterPush = jest.fn(); +jest.mock('next/navigation', () => ({ + useRouter: () => ({ push: mockRouterPush, back: jest.fn() }), + useParams: () => ({ id: 'order-001' }), +})); + +/* Dynamic resource mocks */ +const mockFetchSchema = jest.fn(); +const mockFetchResourceList = jest.fn(); +const mockCreateResource = jest.fn(); +const mockUpdateResource = jest.fn(); +jest.mock('@/lib/dynamic-resource', () => ({ + fetchSchema: (...a: any[]) => mockFetchSchema(...a), + fetchResourceList: (...a: any[]) => mockFetchResourceList(...a), + createResource: (...a: any[]) => mockCreateResource(...a), + updateResource: (...a: any[]) => mockUpdateResource(...a), +})); + +/* API mock */ +const mockApiGet = jest.fn(); +const mockApiPost = jest.fn(); +jest.mock('@/lib/api', () => ({ + __esModule: true, + default: { get: (...a: any[]) => mockApiGet(...a), post: (...a: any[]) => mockApiPost(...a) }, +})); + +/* Child component mocks for DynamicView */ +jest.mock('@/components/core/ListEngine', () => ({ + ListEngine: ({ onRowClick, onSearchChange }: any) => ( +
+ + onSearchChange?.(e.target.value)} /> +
+ ), +})); +jest.mock('@/components/core/KanbanEngine', () => ({ + KanbanEngine: () =>
, +})); +jest.mock('@/components/core/FormEngine', () => ({ + FormEngine: ({ value, onChange, onSubmit }: any) => ( +
+ onChange?.({ ...value, name: e.target.value })} /> + +
+ ), +})); +jest.mock('@/components/ui/Sheet', () => ({ + Sheet: ({ open, children, onClose, title }: any) => + open ? ( +
+ {title} + + {children} +
+ ) : null, +})); + +/* ======================================================== + 测试数据 + ======================================================== */ + +import type { UiSchema } from '@/lib/ui-schema'; + +const mockSchema: UiSchema = { + model: 'Product', label: '产品', description: '产品管理', + fields: [ + { name: 'id', label: 'ID', type: 'string' }, + { name: 'name', label: '名称', type: 'string', required: true }, + { name: 'status', label: '状态', type: 'select', options: [{ label: '草稿', value: 'Draft' }, { label: '已发布', value: 'Published' }] }, + { name: 'categoryId', label: '分类', type: 'reference', reference: { model: 'Category', labelField: 'name', valueField: 'id', relationField: 'category' } }, + ], + views: { + form: { fields: ['name', 'status', 'categoryId'] }, + list: { columns: ['id', 'name', 'status', 'categoryId'], searchFields: ['name'] }, + }, +}; + +const mockListResponse = { + data: [{ id: '1', name: '产品A', status: 'Published' }, { id: '2', name: '产品B', status: 'Draft' }], + total: 2, page: 1, limit: 20, totalPages: 1, +}; + +const mockRefOptions = { + data: [{ id: 'cat-1', name: '电子元器件' }, { id: 'cat-2', name: '机械零件' }], + total: 2, page: 1, limit: 200, totalPages: 1, +}; + +const mockTimelineEvents = { + data: { + events: [ + { id: 'ev1', action: 'CREATED', createdAt: '2025-01-01T00:00:00Z', user: { name: 'admin' } }, + { id: 'ev2', action: 'UPDATED', createdAt: '2025-01-02T00:00:00Z', user: { email: 'user@test.com' } }, + ], + }, +}; + +/* ======================================================== + Smoke 1: DynamicView 核心流程 + ======================================================== */ + +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DynamicView } from '@/components/core/DynamicView'; + +describe('Smoke · DynamicView 核心流程', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorageMock.clear(); + mockFetchSchema.mockResolvedValue(mockSchema); + mockFetchResourceList.mockResolvedValue(mockListResponse); + mockApiGet.mockResolvedValue(mockTimelineEvents); + mockApiPost.mockResolvedValue({}); + mockCreateResource.mockResolvedValue({ id: 'new-1', name: '新产品', status: 'Draft' }); + mockUpdateResource.mockResolvedValue({ id: '1', name: '更新名', status: 'Published' }); + }); + + it('schema 加载成功后显示标题和列表', async () => { + render(); + await waitFor(() => { expect(screen.getByText('产品')).toBeInTheDocument(); }); + expect(screen.getByTestId('list-engine')).toBeInTheDocument(); + }); + + it('reference 字段自动生成 include 参数', async () => { + render(); + await waitFor(() => { expect(mockFetchResourceList).toHaveBeenCalled(); }); + const lastCall = mockFetchResourceList.mock.calls[mockFetchResourceList.mock.calls.length - 1]; + expect(lastCall[1].include).toHaveProperty('category'); + }); + + it('新建记录:点击新建 → 表单打开 → 保存调用 createResource', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + await user.click(screen.getByText('新建 / 编辑')); + await waitFor(() => { expect(screen.getByTestId('sheet')).toBeInTheDocument(); }); + expect(screen.getByTestId('sheet-title').textContent).toContain('新建'); + await user.click(screen.getByText(/保存/)); + await waitFor(() => { expect(mockCreateResource).toHaveBeenCalledWith('Product', expect.objectContaining({})); }); + }); + + it('编辑记录:点击行 → 表单打开 → 保存调用 updateResource', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + await user.click(screen.getByTestId('row-click')); + await waitFor(() => { expect(screen.getByTestId('sheet')).toBeInTheDocument(); }); + expect(screen.getByTestId('sheet-title').textContent).toContain('编辑'); + await user.click(screen.getByText(/保存/)); + await waitFor(() => { + expect(mockUpdateResource).toHaveBeenCalledWith('Product', 'row-1', expect.objectContaining({ id: 'row-1' })); + }); + }); + + it('保存后刷新列表', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + const initCount = mockFetchResourceList.mock.calls.length; + await user.click(screen.getByText('新建 / 编辑')); + await waitFor(() => { expect(screen.getByTestId('sheet')).toBeInTheDocument(); }); + await user.click(screen.getByText(/保存/)); + await waitFor(() => { expect(mockFetchResourceList.mock.calls.length).toBeGreaterThan(initCount); }); + }); + + it('timeline 事件渲染', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + await user.click(screen.getByTestId('row-click')); + await waitFor(() => { + expect(screen.getByText('CREATED')).toBeInTheDocument(); + expect(screen.getByText('admin')).toBeInTheDocument(); + }); + }); + + it('timeline 为空显示占位', async () => { + mockApiGet.mockResolvedValue({ data: { events: [] } }); + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + await user.click(screen.getByTestId('row-click')); + await waitFor(() => { expect(screen.getByText('暂无时间线事件。')).toBeInTheDocument(); }); + }); + + it('提交批注后调用 comment API', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByTestId('list-engine')).toBeInTheDocument(); }); + await user.click(screen.getByTestId('row-click')); + await waitFor(() => { expect(screen.getByPlaceholderText('写入团队批注...')).toBeInTheDocument(); }); + await user.type(screen.getByPlaceholderText('写入团队批注...'), '测试批注'); + await user.click(screen.getByText('发布批注')); + await waitFor(() => { + expect(mockApiPost).toHaveBeenCalledWith('/v1/timeline/Product/row-1/comment', { content: '测试批注' }); + }); + }); + + it('schema 加载失败显示错误', async () => { + mockFetchSchema.mockRejectedValue(new Error('Network error')); + render(); + await waitFor(() => { expect(screen.getByText('Network error')).toBeInTheDocument(); }); + }); +}); + +/* ======================================================== + Smoke 2: 引用字段选择 + ======================================================== */ + +describe('Smoke · 引用字段选择', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorageMock.clear(); + mockFetchSchema.mockResolvedValue(mockSchema); + mockFetchResourceList.mockResolvedValue(mockListResponse); + mockApiGet.mockResolvedValue(mockTimelineEvents); + mockApiPost.mockResolvedValue({}); + }); + + it('schema 含 reference 字段时 fetchResourceList 带 include', async () => { + render(); + await waitFor(() => { expect(mockFetchResourceList).toHaveBeenCalled(); }); + const call = mockFetchResourceList.mock.calls[0]; + expect(call[0]).toBe('Product'); + expect(call[1].include).toEqual({ category: true }); + }); + + it('schema 无 reference 字段时 include 为空对象', async () => { + const noRefSchema: UiSchema = { + ...mockSchema, + fields: mockSchema.fields.filter(f => f.type !== 'reference'), + views: { ...mockSchema.views, list: { ...mockSchema.views.list, columns: ['id', 'name', 'status'] } }, + }; + mockFetchSchema.mockResolvedValue(noRefSchema); + render(); + await waitFor(() => { expect(mockFetchResourceList).toHaveBeenCalled(); }); + const call = mockFetchResourceList.mock.calls[0]; + expect(call[1].include).toEqual({}); + }); +}); + +/* ======================================================== + Smoke 3: 登录页 + ======================================================== */ + +// authStore mock — 同时支持 Login (setAuth) 和 OrderDetail (currentCompanyId) +const mockSetAuth = jest.fn(); +jest.mock('@/store/authStore', () => ({ + useAuthStore: Object.assign( + (selector?: any) => { + const state = { + setAuth: mockSetAuth, + currentCompanyId: 'c1', + token: 'test-token', + user: { id: 'u1', username: 'admin', role: 'admin' }, + companies: [{ id: 'c1', name: 'TestCo', role: 'owner' }], + }; + return typeof selector === 'function' ? selector(state) : state; + }, + { getState: jest.fn() }, + ), +})); + +// 登录页需要单独 mock api(post) +// 已在顶部 mock 了 api.default + +import LoginPage from '@/app/login/page'; + +describe('Smoke · 登录页', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorageMock.clear(); + mockApiPost.mockResolvedValue({ + data: { + accessToken: 'jwt-token-abc', + user: { id: 'u1', username: 'admin', role: 'admin' }, + companies: [{ id: 'c1', name: 'TestCo', role: 'owner' }], + }, + }); + }); + + it('渲染标题和默认账号', () => { + render(); + expect(screen.getByText('登录 OneERP')).toBeInTheDocument(); + const emailInput = screen.getByPlaceholderText('admin@erp.com') as HTMLInputElement; + expect(emailInput.value).toBe('admin@erp.com'); + }); + + it('登录成功调用 API → setAuth → 跳转', async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByText('进入工作台')); + await waitFor(() => { + expect(mockApiPost).toHaveBeenCalledWith('/auth/login', { email: 'admin@erp.com', password: 'admin' }); + }); + await waitFor(() => { + expect(mockSetAuth).toHaveBeenCalledWith('jwt-token-abc', expect.any(Object), expect.any(Array)); + }); + await waitFor(() => { + expect(mockRouterPush).toHaveBeenCalledWith('/dashboard'); + }); + }); + + it('登录失败显示错误', async () => { + mockApiPost.mockRejectedValueOnce({ response: { data: { message: '用户名或密码错误' } } }); + const user = userEvent.setup(); + render(); + await user.click(screen.getByText('进入工作台')); + await waitFor(() => { + expect(screen.getByText('用户名或密码错误')).toBeInTheDocument(); + }); + }); +}); + +/* ======================================================== + Smoke 4: 订单详情 Timeline + ======================================================== */ + +const mockToastError = jest.fn(); +const mockToastSuccess = jest.fn(); +jest.mock('react-hot-toast', () => ({ + __esModule: true, + default: { error: (...a: any[]) => mockToastError(...a), success: (...a: any[]) => mockToastSuccess(...a) }, +})); + +const mockOrderData = { + id: 'order-001', orderNo: 'SO-2025-0001', status: 'DRAFT', totalAmount: 50000, + expectedDate: '2025-06-01T00:00:00Z', notes: '优先安排', createdAt: '2025-01-15T08:00:00Z', + partner: { id: 'p1', name: '测试客户A', contact: '张三', phone: '13800000000' }, + salesPerson: { id: 'sp1', name: '李销售' }, + items: [{ id: 'item-1', productId: 'PROD-001', quantity: 10, unitPrice: 5000, totalPrice: 50000 }], + workOrders: [], invoices: [], +}; + +import OrderDetailPage from '@/app/dashboard/orders/[id]/page'; + +describe('Smoke · 订单详情 Timeline', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorageMock.clear(); + mockApiGet.mockImplementation((url: string) => { + if (url === '/orders/order-001') return Promise.resolve({ data: mockOrderData }); + if (url === '/orders/order-001/timeline') return Promise.resolve({ + data: { events: [{ id: 'ev1', action: 'CREATED', createdAt: '2025-01-15T08:00:00Z', user: { name: 'admin' } }] }, + }); + return Promise.resolve({ data: {} }); + }); + mockApiPost.mockResolvedValue({}); + }); + + it('加载订单号和状态', async () => { + render(); + await waitFor(() => { + expect( + screen.getByText((_, element) => element?.textContent === '订单 SO-2025-0001'), + ).toBeInTheDocument(); + }); + expect(screen.getByText('草稿')).toBeInTheDocument(); + }); + + it('Timeline 事件渲染', async () => { + render(); + await waitFor(() => { expect(screen.getByText('CREATED')).toBeInTheDocument(); }); + expect(screen.getByText('admin')).toBeInTheDocument(); + }); + + it('Timeline 为空显示占位', async () => { + mockApiGet.mockImplementation((url: string) => { + if (url === '/orders/order-001') return Promise.resolve({ data: mockOrderData }); + return Promise.resolve({ data: { events: [] } }); + }); + render(); + await waitFor(() => { expect(screen.getByText('暂无动态记录。')).toBeInTheDocument(); }); + }); + + it('状态流转触发 workflow API', async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => { expect(screen.getByText('提交订单')).toBeInTheDocument(); }); + await user.click(screen.getByText('提交订单')); + await waitFor(() => { + expect(mockApiPost).toHaveBeenCalledWith('/v1/workflow/order/order-001/transition', { action: 'submit' }); + }); + }); + + it('加载失败跳转回列表', async () => { + mockApiGet.mockImplementation((url: string) => { + if (url === '/orders/order-001') return Promise.reject(new Error('fail')); + return Promise.resolve({ data: { events: [] } }); + }); + render(); + await waitFor(() => { expect(mockRouterPush).toHaveBeenCalledWith('/dashboard/orders'); }); + }); +}); diff --git a/apps/web/src/app/dashboard/DashboardClient.tsx b/apps/web/src/app/dashboard/DashboardClient.tsx index d2a32a2..f182be7 100644 --- a/apps/web/src/app/dashboard/DashboardClient.tsx +++ b/apps/web/src/app/dashboard/DashboardClient.tsx @@ -7,6 +7,15 @@ import { Activity, Box, CreditCard, ShoppingBag, Sparkles, Truck } from 'lucide- import { Sheet } from '../../components/ui/Sheet'; import { Chat2DashPanel } from '../../components/ai/Chat2DashPanel'; +interface RecentOrder { + id: string; + orderNo: string; + status: string; + partner?: { + name?: string; + } | null; +} + export default function DashboardClient() { const { currentCompanyId, token } = useAuthStore(); const [chat2DashOpen, setChat2DashOpen] = useState(false); @@ -17,7 +26,7 @@ export default function DashboardClient() { lowStockItems: 0 }); - const [recentOrders, setRecentOrders] = useState([]); + const [recentOrders, setRecentOrders] = useState([]); useEffect(() => { const fetchStats = async () => { @@ -28,7 +37,6 @@ export default function DashboardClient() { ]); setStats(statsResponse.data); - // 兼容两种返回格式: 直接数组 或 分页对象 { data, total, ... } const ordersPayload = ordersResponse.data; const orders = Array.isArray(ordersPayload) ? ordersPayload @@ -72,7 +80,7 @@ export default function DashboardClient() {
-

生产品产中

+

进行中订单

{stats.activeOrders}

@@ -129,8 +137,8 @@ export default function DashboardClient() {

系统公告

-

欢迎使用现代企业EIP系统。您的账号已绑定多公司管理权限,请在左侧侧边栏切换需要管理的企业数据域。

-

系统已升级至最新版本,全面支持基于隔离数据沙箱的库存和订单流转。

+

欢迎使用 OneERP 业务工作台。当前账号已绑定多公司权限,可在左侧切换需要管理的企业数据域。

+

系统支持订单、库存、采购和财务的统一流转,关键操作会保留审计记录。

diff --git a/apps/web/src/app/dashboard/finance/trial-balance/page.tsx b/apps/web/src/app/dashboard/finance/trial-balance/page.tsx new file mode 100644 index 0000000..b5e4453 --- /dev/null +++ b/apps/web/src/app/dashboard/finance/trial-balance/page.tsx @@ -0,0 +1,143 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import api from '@/lib/api'; +import { usePermissions, RequirePermission } from '@/lib/permissions-context'; + +interface TrialBalanceRow { + accountId: string; + accountNo: string; + accountName: string; + accountType: string; + debit: number; + credit: number; +} + +export default function TrialBalancePage() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [error, setError] = useState(null); + + const fetchData = async () => { + setLoading(true); + setError(null); + try { + const res = await api.get('/v1/finance/trial-balance', { + params: { + startDate: startDate || undefined, + endDate: endDate || undefined, + }, + }); + setData(res.data); + } catch (e: any) { + setError(e?.response?.data?.message || '加载失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const totalDebit = data.reduce((sum, row) => sum + row.debit, 0); + const totalCredit = data.reduce((sum, row) => sum + row.credit, 0); + const isBalanced = Math.abs(totalDebit - totalCredit) < 0.01; + + return ( +
+
+

试算平衡表 (Trial Balance)

+ +
+ setStartDate(e.target.value)} + className="text-sm border border-gray-300 rounded-md px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + - + setEndDate(e.target.value)} + className="text-sm border border-gray-300 rounded-md px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + +
+
+ +
+ {error ? ( +
{error}
+ ) : ( + + + + + + + + + + + + {data.map((row) => ( + + + + + + + + ))} + {data.length === 0 && !loading && ( + + + + )} + + {data.length > 0 && ( + + + + + + + + )} +
科目代码科目名称科目类型借方合计 (Debit)贷方合计 (Credit)
{row.accountNo}{row.accountName}{row.accountType} + {row.debit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + {row.credit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} +
+ 没有找到符合条件的分录 +
+ 总计: + + {totalDebit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + + {totalCredit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} +
+ )} + + {data.length > 0 && ( +
+ 试算状态:{isBalanced ? '平衡' : '不平衡'} + {!isBalanced && ( + 差额:{Math.abs(totalDebit - totalCredit).toLocaleString('zh-CN', { minimumFractionDigits: 2 })} + )} +
+ )} +
+
+ ); +} diff --git a/apps/web/src/app/dashboard/inventory/page.tsx b/apps/web/src/app/dashboard/inventory/page.tsx index 230b242..669c65d 100644 --- a/apps/web/src/app/dashboard/inventory/page.tsx +++ b/apps/web/src/app/dashboard/inventory/page.tsx @@ -1,20 +1,20 @@ 'use client'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import api from '@/lib/api'; import { - Package, - Warehouse, AlertTriangle, - RefreshCw, - ChevronRight, + CheckCircle2, ChevronDown, + ChevronRight, + Loader2, + Package, + RefreshCw, Scan, Search, - CheckCircle2, - Loader2, + Warehouse, } from 'lucide-react'; import toast from 'react-hot-toast'; +import api from '@/lib/api'; interface LedgerRow { locationId: string; @@ -46,37 +46,39 @@ interface LocationGroup { lowCount: number; } -/** Build a two-level tree: Warehouse > Location > rows */ function buildTree(rows: LedgerRow[]): WarehouseGroup[] { - const whMap = new Map(); + const warehouseMap = new Map(); for (const row of rows) { - const whKey = row.warehouseId ?? '__NO_WH__'; - if (!whMap.has(whKey)) { - whMap.set(whKey, { + const warehouseKey = row.warehouseId ?? '__NO_WAREHOUSE__'; + let warehouse = warehouseMap.get(warehouseKey); + + if (!warehouse) { + warehouse = { warehouseId: row.warehouseId, - warehouseName: row.warehouseName ?? '(未分配仓库)', + warehouseName: row.warehouseName ?? '未分配仓库', locations: [], totalRows: 0, lowCount: 0, - }); + }; + warehouseMap.set(warehouseKey, warehouse); } - const wh = whMap.get(whKey)!; - let loc = wh.locations.find((l) => l.locationId === row.locationId); - if (!loc) { - loc = { locationId: row.locationId, locationName: row.locationName, rows: [], lowCount: 0 }; - wh.locations.push(loc); + let location = warehouse.locations.find((item) => item.locationId === row.locationId); + if (!location) { + location = { locationId: row.locationId, locationName: row.locationName, rows: [], lowCount: 0 }; + warehouse.locations.push(location); } - loc.rows.push(row); + + location.rows.push(row); + warehouse.totalRows += 1; if (row.isLow) { - loc.lowCount++; - wh.lowCount++; + location.lowCount += 1; + warehouse.lowCount += 1; } - wh.totalRows++; } - return Array.from(whMap.values()).sort((a, b) => + return Array.from(warehouseMap.values()).sort((a, b) => a.warehouseName.localeCompare(b.warehouseName, 'zh-CN'), ); } @@ -84,12 +86,13 @@ function buildTree(rows: LedgerRow[]): WarehouseGroup[] { function QtyCell({ row }: { row: LedgerRow }) { if (row.isLow) { return ( - + {row.netQty.toLocaleString()} {row.materialUnit} ); } + return ( {row.netQty.toLocaleString()} {row.materialUnit} @@ -101,10 +104,8 @@ export default function InventoryPage() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); - const [expandedWh, setExpandedWh] = useState>(new Set()); - const [expandedLoc, setExpandedLoc] = useState>(new Set()); - - // Barcode scan mode + const [expandedWarehouses, setExpandedWarehouses] = useState>(new Set()); + const [expandedLocations, setExpandedLocations] = useState>(new Set()); const [scanMode, setScanMode] = useState(false); const [scanBuffer, setScanBuffer] = useState(''); const [scanning, setScanning] = useState(false); @@ -113,8 +114,8 @@ export default function InventoryPage() { const fetchLedger = useCallback(async () => { setLoading(true); try { - const res = await api.get('/inventory/realtime-ledger'); - setRows(res.data ?? []); + const response = await api.get('/inventory/realtime-ledger'); + setRows(response.data ?? []); } catch (error) { console.error('Failed to fetch realtime ledger', error); toast.error('加载库存台账失败'); @@ -124,85 +125,81 @@ export default function InventoryPage() { }, []); useEffect(() => { - fetchLedger(); + void fetchLedger(); }, [fetchLedger]); - // Filter rows by search const filteredRows = useMemo(() => { - if (!search.trim()) return rows; - const kw = search.toLowerCase(); + const keyword = search.trim().toLowerCase(); + if (!keyword) return rows; + return rows.filter( - (r) => - r.materialName.toLowerCase().includes(kw) || - r.materialSku.toLowerCase().includes(kw) || - r.locationName.toLowerCase().includes(kw) || - (r.warehouseName ?? '').toLowerCase().includes(kw), + (row) => + row.materialName.toLowerCase().includes(keyword) || + row.materialSku.toLowerCase().includes(keyword) || + row.locationName.toLowerCase().includes(keyword) || + (row.warehouseName ?? '').toLowerCase().includes(keyword), ); }, [rows, search]); const tree = useMemo(() => buildTree(filteredRows), [filteredRows]); - // Auto-expand all warehouses and locations when data first arrives - const hasData = rows.length > 0; useEffect(() => { - if (!hasData || tree.length === 0) return; - const newWh = new Set(); - const newLoc = new Set(); - for (const wh of tree) { - newWh.add(wh.warehouseId ?? '__NO_WH__'); - for (const loc of wh.locations) { - newLoc.add(loc.locationId); - } + if (rows.length === 0 || tree.length === 0) return; + + const warehouseKeys = new Set(); + const locationKeys = new Set(); + for (const warehouse of tree) { + warehouseKeys.add(warehouse.warehouseId ?? '__NO_WAREHOUSE__'); + for (const location of warehouse.locations) locationKeys.add(location.locationId); } - setExpandedWh(newWh); - setExpandedLoc(newLoc); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasData]); - - const toggleWh = (key: string) => { - setExpandedWh((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } + + setExpandedWarehouses(warehouseKeys); + setExpandedLocations(locationKeys); + // Expand only when initial data arrives; user toggles should not be reset on every search. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rows.length]); + + const totalMaterials = useMemo(() => new Set(rows.map((row) => row.materialId)).size, [rows]); + const totalLocations = useMemo(() => new Set(rows.map((row) => row.locationId)).size, [rows]); + const totalLow = useMemo(() => rows.filter((row) => row.isLow).length, [rows]); + + const toggleWarehouse = (key: string) => { + setExpandedWarehouses((previous) => { + const next = new Set(previous); + if (next.has(key)) next.delete(key); + else next.add(key); return next; }); }; - const toggleLoc = (key: string) => { - setExpandedLoc((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } + const toggleLocation = (key: string) => { + setExpandedLocations((previous) => { + const next = new Set(previous); + if (next.has(key)) next.delete(key); + else next.add(key); return next; }); }; - // Summary stats - const totalMaterials = useMemo(() => new Set(rows.map((r) => r.materialId)).size, [rows]); - const totalLocations = useMemo(() => new Set(rows.map((r) => r.locationId)).size, [rows]); - const totalLow = useMemo(() => rows.filter((r) => r.isLow).length, [rows]); - - // Barcode scan handler const handleScanSubmit = useCallback( async (sku: string) => { - if (!sku.trim()) return; + const materialSku = sku.trim(); + if (!materialSku) return; + setScanning(true); try { - await api.post('/inventory/scan', { materialSku: sku.trim(), quantity: 1 }); - toast.success(`扫码出库成功:${sku.trim()}`); + await api.post('/inventory/scan', { materialSku, quantity: 1 }); + toast.success(`扫码出库成功:${materialSku}`); await fetchLedger(); - } catch (err: unknown) { - const msg = - err && typeof err === 'object' && 'response' in err - ? String((err as { response: { data?: { message?: string } } }).response?.data?.message ?? '出库失败') + } catch (error: unknown) { + const message = + error && typeof error === 'object' && 'response' in error + ? String( + (error as { response?: { data?: { message?: string } } }).response?.data?.message ?? + '出库失败', + ) : '出库失败'; - toast.error(msg); + toast.error(message); } finally { setScanning(false); setScanBuffer(''); @@ -214,7 +211,6 @@ export default function InventoryPage() { return (
- {/* Header */}

@@ -222,7 +218,7 @@ export default function InventoryPage() { 实时库存台账

- 基于 Kysely 聚合查询,支持层级钻取与低库存预警。 + 基于聚合查询的库存视图,支持层级钻取与低库存预警。

@@ -230,10 +226,8 @@ export default function InventoryPage() {
- {/* Barcode Scan Panel */} {scanMode && (
- 扫码枪出库模式 – 扫入物料条码后自动扣减 1 件库存 + 扫码枪出库模式:扫入物料条码后自动扣减 1 件库存。
setScanBuffer(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - void handleScanSubmit(scanBuffer); - } + onChange={(event) => setScanBuffer(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') void handleScanSubmit(scanBuffer); }} placeholder="将光标聚焦此处,然后扫码..." className="h-9 flex-1 rounded-lg border border-blue-300 bg-white px-3 text-sm outline-none focus:ring-2 focus:ring-blue-200" @@ -283,15 +274,18 @@ export default function InventoryPage() { onClick={() => void handleScanSubmit(scanBuffer)} className="inline-flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" > - {scanning ? : } + {scanning ? ( + + ) : ( + + )} 确认
)} - {/* Stats Bar */} -
+
@@ -301,6 +295,7 @@ export default function InventoryPage() {
{totalMaterials}
+
@@ -310,6 +305,7 @@ export default function InventoryPage() {
{totalLocations}
+
0 ? 'border-amber-200 bg-amber-50/60' : ''}`}>
0 ? 'bg-amber-100 text-amber-600' : 'bg-green-50 text-green-600'}`}> @@ -323,26 +319,25 @@ export default function InventoryPage() {
- {/* Search */}
setSearch(e.target.value)} + onChange={(event) => setSearch(event.target.value)} placeholder="按物料名、SKU、库位搜索..." className="h-9 w-full rounded-lg border border-slate-200 bg-white pl-9 pr-3 text-sm outline-none focus:ring-2 focus:ring-blue-100" />
- {/* Ledger Table */}
- {/* Table header */} -
+
物料
SKU
-
净库存量
+
净库存
最低库存
批次数
状态
@@ -356,62 +351,54 @@ export default function InventoryPage() { ) : tree.length === 0 ? (
暂无库存数据
) : ( - tree.map((wh) => { - const whKey = wh.warehouseId ?? '__NO_WH__'; - const whOpen = expandedWh.has(whKey); + tree.map((warehouse) => { + const warehouseKey = warehouse.warehouseId ?? '__NO_WAREHOUSE__'; + const warehouseOpen = expandedWarehouses.has(warehouseKey); return ( -
- {/* Warehouse row */} +
- {whOpen && - wh.locations.map((loc) => { - const locOpen = expandedLoc.has(loc.locationId); + {warehouseOpen && + warehouse.locations.map((location) => { + const locationOpen = expandedLocations.has(location.locationId); + return ( -
- {/* Location row */} +
- {/* Material rows */} - {locOpen && - loc.rows.map((row) => ( + {locationOpen && + location.rows.map((row) => (
- 共 {filteredRows.length} 条库存明细 · 单击仓库/库位行展开/折叠 + 共 {filteredRows.length} 条库存明细 · 单击仓库/库位行展开或折叠
); diff --git a/apps/web/src/app/dashboard/inventory/receiving/page.tsx b/apps/web/src/app/dashboard/inventory/receiving/page.tsx new file mode 100644 index 0000000..bd8c292 --- /dev/null +++ b/apps/web/src/app/dashboard/inventory/receiving/page.tsx @@ -0,0 +1,518 @@ +'use client'; + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import api from '@/lib/api'; +import { + AlertTriangle, + CheckCircle2, + ChevronDown, + Hash, + Loader2, + MapPin, + Package, + Scan, + Search, + Truck, +} from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface Material { + id: string; + sku: string; + name: string; + unit: string; +} + +interface PurchaseOrderItem { + id: string; + materialId: string; + quantity: number; + receivedQty: number; + remainingQty: number; + unitPrice: number; + material: Material; +} + +interface Supplier { + id: string; + name: string; +} + +interface PurchaseOrder { + id: string; + orderNo: string; + status: string; + supplier: Supplier; + items: PurchaseOrderItem[]; + createdAt: string; +} + +interface Warehouse { + id: string; + name: string; +} + +interface StockLocation { + id: string; + name: string; + warehouseId: string | null; +} + +function StatusBadge({ status }: { status: string }) { + const map: Record = { + CONFIRMED: { label: '已确认', cls: 'erp-badge--info' }, + PARTIALLY_RECEIVED: { label: '部分收货', cls: 'erp-badge--pending' }, + RECEIVED: { label: '已收货', cls: 'erp-badge--success' }, + }; + const s = map[status] ?? { label: status, cls: '' }; + return {s.label}; +} + +export default function ReceivingPage() { + const [orders, setOrders] = useState([]); + const [locations, setLocations] = useState([]); + const [warehouses, setWarehouses] = useState([]); + const [loading, setLoading] = useState(true); + + const [selectedOrderId, setSelectedOrderId] = useState(''); + const [selectedItemId, setSelectedItemId] = useState(''); + const [selectedLocationId, setSelectedLocationId] = useState(''); + const [selectedWarehouseId, setSelectedWarehouseId] = useState(''); + + const [quantity, setQuantity] = useState(''); + const [batchNo, setBatchNo] = useState(''); + const [note, setNote] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const [scanMode, setScanMode] = useState(false); + const [scanBuffer, setScanBuffer] = useState(''); + const scanInputRef = useRef(null); + + const fetchOrders = useCallback(async () => { + setLoading(true); + try { + const res = await api.get('/purchase/orders/pending'); + setOrders(res.data ?? []); + } catch { + toast.error('加载采购单失败'); + } finally { + setLoading(false); + } + }, []); + + const fetchLocations = useCallback(async () => { + try { + const [whRes, locRes] = await Promise.all([ + api.get('/inventory/warehouses'), + api.get('/inventory/locations'), + ]); + setWarehouses(whRes.data ?? []); + setLocations(locRes.data ?? []); + } catch { + toast.error('加载库位信息失败'); + } + }, []); + + useEffect(() => { + void fetchOrders(); + void fetchLocations(); + }, [fetchOrders, fetchLocations]); + + const selectedOrder = useMemo( + () => orders.find((o) => o.id === selectedOrderId), + [orders, selectedOrderId], + ); + + const selectedItem = useMemo( + () => selectedOrder?.items.find((i) => i.id === selectedItemId), + [selectedOrder, selectedItemId], + ); + + const filteredLocations = useMemo(() => { + if (!selectedWarehouseId) return locations; + return locations.filter((l) => l.warehouseId === selectedWarehouseId); + }, [locations, selectedWarehouseId]); + + const qtyError = useMemo(() => { + if (!quantity || !selectedItem) return null; + const num = Number(quantity); + if (Number.isNaN(num) || num <= 0) return '收货数量必须大于 0'; + if (num > selectedItem.remainingQty) return `不能超过待收数量 (${selectedItem.remainingQty})`; + return null; + }, [quantity, selectedItem]); + + const isValid = !!selectedOrderId && !!selectedItemId && !!selectedLocationId && !!quantity && !qtyError; + + const handleSubmit = async () => { + if (!isValid || !selectedOrder || !selectedItem) return; + setSubmitting(true); + try { + const payload = { + purchaseOrderId: selectedOrder.id, + itemId: selectedItem.id, + quantity: Number(quantity), + destLocationId: selectedLocationId, + batchNo: batchNo.trim() || undefined, + note: note.trim() || undefined, + }; + const res = await api.post('/purchase/orders/receive', payload); + toast.success(res.data?.message ?? '收货成功'); + setQuantity(''); + setBatchNo(''); + setNote(''); + setSelectedItemId(''); + await fetchOrders(); + } catch (err: unknown) { + const msg = + err && typeof err === 'object' && 'response' in err + ? String((err as { response: { data?: { message?: string } } }).response?.data?.message ?? '收货失败') + : '收货失败'; + toast.error(msg); + } finally { + setSubmitting(false); + } + }; + + const handleScan = async (sku: string) => { + if (!sku.trim() || !selectedLocationId) { + toast.error('请先选择目标库位'); + return; + } + setSubmitting(true); + try { + const payload = { + materialSku: sku.trim(), + quantity: 1, + destLocationId: selectedLocationId, + batchNo: batchNo.trim() || undefined, + }; + const res = await api.post('/purchase/orders/scan-receive', payload); + toast.success(res.data?.message ?? '扫码收货成功'); + await fetchOrders(); + } catch (err: unknown) { + const msg = + err && typeof err === 'object' && 'response' in err + ? String((err as { response: { data?: { message?: string } } }).response?.data?.message ?? '扫码收货失败') + : '扫码收货失败'; + toast.error(msg); + } finally { + setSubmitting(false); + setScanBuffer(''); + scanInputRef.current?.focus(); + } + }; + + return ( +
+
+
+

+ + 收货执行 +

+

+ 选择采购单、库位、批次和数量完成收货入库,支持扫码枪快速收货。 +

+
+
+ + +
+
+ + {scanMode && ( +
+
+ + 扫码枪收货模式:扫描物料编码后自动匹配待收采购单并收货 1 件 +
+
+ setScanBuffer(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') void handleScan(scanBuffer); + }} + placeholder="光标停在此处后扫码..." + className="h-9 flex-1 rounded-lg border border-blue-300 bg-white px-3 text-sm outline-none focus:ring-2 focus:ring-blue-200" + /> + +
+ {!selectedLocationId && ( +

+ + 请先在下方选择目标库位再扫码 +

+ )} +
+ )} + +
+
+

+ + 收货信息 +

+ +
+ +
+ + +
+
+ + {selectedOrder && ( +
+ +
+ + +
+
+ )} + + {selectedItem && ( +
+
+ 采购数量 + {selectedItem.quantity} {selectedItem.material.unit} +
+
+ 已收货 + {selectedItem.receivedQty} {selectedItem.material.unit} +
+
+ 待收货 + {selectedItem.remainingQty} {selectedItem.material.unit} +
+
+ )} + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + setBatchNo(e.target.value)} + placeholder="输入或扫描批次号,留空则自动生成" + className="h-10 w-full rounded-lg border border-slate-200 bg-white px-3 text-sm outline-none focus:ring-2 focus:ring-blue-100" + /> +
+ +
+ + setQuantity(e.target.value)} + placeholder={selectedItem ? `最多 ${selectedItem.remainingQty}` : '请输入数量'} + min="0.01" + max={selectedItem?.remainingQty} + step="0.01" + className={`h-10 w-full rounded-lg border bg-white px-3 text-sm outline-none focus:ring-2 ${ + qtyError ? 'border-red-300 focus:ring-red-100' : 'border-slate-200 focus:ring-blue-100' + }`} + /> + {qtyError && ( +

+ + {qtyError} +

+ )} +
+ +
+ +