diff --git a/.gitignore b/.gitignore index 41a40683..0144b66a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -/src/.idea/ +/src/.idea/ bin obj *.user -/dist/ \ No newline at end of file +/dist/ +server.env +certs/ +docker-registry-config.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..a2455a6e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/Hosts/NVs.Budget.Hosts.Web.Server/bin/Debug/net8.0/NVs.Budget.Hosts.Web.Server.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Hosts/NVs.Budget.Hosts.Web.Server", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..327b8080 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/NVs.Budget.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/NVs.Budget.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/NVs.Budget.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..288c3cf8 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,222 @@ +# Local Development Guide + +This guide covers setting up and running the Budget application for local development. + +## Prerequisites + +### Required Software +- Docker Desktop +- PowerShell 7+ (for Windows) or Bash (for Linux/Mac) +- .NET SDK 8.0+ +- Node.js 20+ +- npm + +## Quick Start + +The easiest way to start development is using the scripts in `src/Hosts/web-debug/`: + +```powershell +cd src/Hosts/web-debug +.\start-all.ps1 +``` + +This will: +1. Start PostgreSQL database in Docker +2. Generate SSL certificates +3. Launch the .NET server with watch mode +4. Launch the Angular client with watch mode + +## Configuration + +### 1. Environment Setup + +1. Navigate to `src/Hosts/web-debug/` +2. Copy `server.env.example` to `server.env` +3. Fill in your Yandex OAuth credentials: + ``` + Auth__Yandex__ClientSecret = your_client_secret + Auth__Yandex__ClientId = your_client_id + ``` + +### 2. Database Setup + +The development setup uses Docker Compose to run PostgreSQL. The database is automatically created when you run `docker compose up -d`. + +**Connection Details:** +- Host: `localhost` +- Port: `20000` +- Database: `budgetdb` +- User: `postgres` +- Password: `postgres` + +## Service URLs + +Once all services are running: + +- **Server (HTTPS)**: https://localhost:7237 +- **Server (HTTP)**: http://localhost:5153 +- **Client**: https://localhost:4200 +- **PostgreSQL**: localhost:20000 + +## Development Workflow + +1. **Start all services**: + ```powershell + cd src/Hosts/web-debug + .\start-all.ps1 + ``` + +2. **Make changes** to your code in the editor + +3. **Watch mode automatically reloads**: + - **.NET Server**: `dotnet watch` detects changes and rebuilds/restarts + - **Angular Client**: Hot Module Replacement (HMR) updates the browser + +4. **View changes** in your browser at http://localhost:4200 + +## Starting Services Individually + +### Start only Docker dependencies +```powershell +cd src/Hosts/web-debug +docker compose up -d +``` + +### Start only the server +```powershell +cd src/Hosts/web-debug +.\start-server.ps1 +``` + +### Start only the client +```powershell +cd src/Hosts/web-debug +.\start-client.ps1 +``` + +### Stop all services +```powershell +cd src/Hosts/web-debug +.\stop-all.ps1 +``` + +Then manually stop server/client processes (Ctrl+C in their windows). + +## Database Management + +### Apply Migrations + +After starting the services, apply database migrations: +```bash +GET https://localhost:7237/admin/patch-db +``` + +Or use curl: +```bash +curl -k https://localhost:7237/admin/patch-db +``` + +### Reset Database + +To completely reset the database (removes all data): +```powershell +cd src/Hosts/web-debug +docker compose down -v +``` + +This removes the Docker volume containing PostgreSQL data. The database will be recreated on next start. + +## Troubleshooting + +### Certificate Issues + +If you encounter SSL certificate errors: + +```powershell +cd src/Hosts/web-debug +# Delete the certs directory +Remove-Item -Recurse -Force .\certs + +# Restart services to regenerate certificates +.\start-all.ps1 +``` + +### Database Connection Issues + +```powershell +cd src/Hosts/web-debug +# Check if PostgreSQL is running +docker compose ps + +# View PostgreSQL logs +docker compose logs postgres + +# Restart PostgreSQL +docker compose restart postgres +``` + +### Port Already in Use + +If ports are already in use, stop any conflicting services: +- Server: 7237 (HTTPS), 5153 (HTTP) +- Client: 4200 +- PostgreSQL: 20000 + +### Server Won't Start + +1. Check that PostgreSQL is running: `docker compose ps` +2. Verify connection strings in `server.env` +3. Check server logs in the PowerShell window where it's running +4. Ensure ports 7237 and 5153 are not in use + +### Client Won't Start + +1. Verify Node.js and npm are installed: `node --version` and `npm --version` +2. Check that port 4200 is not in use +3. Try deleting `node_modules` and reinstalling: + ```bash + cd src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client + rm -rf node_modules + npm install + ``` + +## Project Structure + +``` +src/Hosts/web-debug/ +├── docker-compose.yml # Docker services (postgres, dev-certs) +├── dev-certs.Dockerfile # Certificate generation +├── server.env # Server environment variables (gitignored) +├── server.env.example # Template for server.env +├── start-all.ps1 # Master script to start everything +├── start-server.ps1 # Start .NET server on host +├── start-client.ps1 # Start Angular client on host +├── stop-all.ps1 # Stop Docker services +└── README.md # Additional details +``` + +## Docker Volumes + +The development setup creates persistent Docker volumes: + +- `web-debug_budgetdb-data`: PostgreSQL data (persistent across restarts) +- `web-debug_certs`: SSL certificates + +To remove all volumes and start fresh: +```powershell +docker compose down -v +``` + +## Benefits of This Development Setup + +✅ **Fast Iteration**: Watch mode catches changes instantly +✅ **Better Debugging**: Direct access to processes on host +✅ **Isolated Dependencies**: Database runs in Docker +✅ **Flexible Development**: Start/stop services independently +✅ **Production-like**: SSL certificates and proper configuration + +## Additional Resources + +For more detailed information about the development scripts, see: +- [src/Hosts/web-debug/README.md](src/Hosts/web-debug/README.md) - Detailed documentation of development scripts + diff --git a/README.md b/README.md index 9c5b05d5..743445b0 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,292 @@ # NV's budget Yet another budget tracking tool. Successor of [flow](https://github.com/nvsnkv/flow2). The main features are: +* Web UI to manage data! * Import data from any CSV list of operations provided by your banks. `budget` provides uniform way of storing this data -* Find and mark transfers between your accounts to exclude them from "incomes" and "withdraws", if that was a transfer from your account to another your account +* Find and mark transfers between your accounts within a budget to exclude them from "incomes" and "withdraws" * Tag operations in a way _you_ need with powerful tagging criteria * Build calendar-like aggregation of your operations to track your expences month-to-monht ## How to use it? +0. Configure authentication service in [Yandex OAuth](https://oauth.yandex.ru/). Service currently supports only yandex oauth provider. +1. Host budget web services (client bundle and server-side app). Deployment options are described below. +2. Configre budget settings (import options, tagging and transfer ctriteria) +3. Configure aggregation rules (logbook criteria) -### Prerequisites -Budget still needs a PostgreSQL database to store the data. You can use `docker-compose.yml` from [src/Hosts](./src/Hosts/) folder to spin up new database but please make sure you changed password! -You'll also need a [.NET 8 runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) for your OS. +Starting from this point you're ready to use service: -Optionally, you'll need a Rider IDE if you would like to build it on your own (publish script is currently rider-specific). Any other IDE for .NET will work, but you'll need to figure out how to publish on your own. +4. Import new operations +5. Handle possible duplicates and transfers +6. Build and explore agregated expenses -### Installation +### Hosting -At some point I'll start publishing releases to github, but for now you'll need to clone repository and build [./src/Hosts/NVs.Budget.Hosts.Console](https://github.com/nvsnkv/budget/blob/console/src/Hosts/NVs.Budget.Hosts.Console/NVs.Budget.Hosts.Console.csproj) project - it's an application entry point. Once compiled, you need to update application settings (define a connection string to database at least) and then you should be ready to explore `budget` features. +The application consists of two main components that need to be hosted: + +1. **Web Server** (`NVs.Budget.Hosts.Web.Server`): .NET 8.0 ASP.NET Core API server +2. **Web Client** (`NVs.Budget.Hosts.Web.Client`): Angular application served as static files + +#### Prerequisites + +- PostgreSQL 17+ database +- .NET 8.0 SDK (for building) +- Node.js 20+ and npm (for building client) +- Docker (optional, for containerized deployment) + +#### Configuration + +The server requires the following configuration (via environment variables or `appsettings.json`): + +- **Connection Strings**: + - `ConnectionStrings:IdentityContext` - PostgreSQL connection string for identity/authentication data + - `ConnectionStrings:BudgetContext` - PostgreSQL connection string for budget data +- **Authentication**: + - `Auth:Yandex:ClientId` - Yandex OAuth client ID + - `Auth:Yandex:ClientSecret` - Yandex OAuth client secret +- **Frontend**: + - `FrontendUrl` - URL where the client application is hosted + - `AllowedOrigins` - Semicolon-separated list of allowed CORS origins + +#### Docker Deployment + +Both server and client have Dockerfiles for containerized deployment: + +- **Server**: Exposes ports 7237 (HTTPS) and 5153 (HTTP) +- **Client**: Exposes ports 8080 (HTTP) and 8081 (HTTPS) + +Build and run: +```bash +# Build server +docker build -f src/Hosts/NVs.Budget.Hosts.Web.Server/Dockerfile -t budget-server . + +# Build client +docker build -f src/Hosts/NVs.Budget.Hosts.Web.Client/Dockerfile -t budget-client . + +# Run with docker-compose (create your own compose file) +``` + +#### Development Setup + +For local development, see [DEVELOPMENT.md](DEVELOPMENT.md) for detailed setup instructions. + +#### Database Migration + +After deployment, run database migrations: +```bash +GET /admin/patch-db +``` + +This endpoint applies all pending migrations to both identity and budget databases. + +### Budget settings + +Each budget has three types of settings that control how operations are processed: + +#### 1. File Reading Settings (CSV Import Configuration) + +Configure how CSV files from your banks are parsed. Each setting is associated with a file pattern (regex) and includes: + +- **Culture**: Locale for parsing numbers and dates (e.g., `en-US`, `ru-RU`) +- **Encoding**: Text encoding of the CSV file (e.g., `UTF-8`, `Windows-1251`) +- **DateTime Kind**: How to interpret date/time values (`Local`, `Utc`, `Unspecified`) +- **Field Mappings**: Maps CSV column indices/names to operation properties: + - `Amount` - Transaction amount + - `Currency` - Currency code + - `Timestamp` - Transaction date/time + - `Description` - Transaction description +- **Attribute Mappings**: Maps CSV columns to custom operation attributes like MCC codes or whatever else you need and have in raw data +- **Validation Rules**: Rules to validate and filter CSV rows: + - Condition: `Equals` or `NotEquals` + - Field: Column to check + - Value: Expected value + +#### 2. Tagging Criteria + +Define rules that automatically assign tags to operations based on conditions. Each criterion consists of: + +- **Tag Expression**: Expression that computes the tag name from operation properties. + - Example: `o => o.Description.Contains("Grocery") ? "Food" : "Other"` +- **Condition**: Boolean expression that determines when to apply the tag + - Example: `o => o.Amount.Amount < 0` (only for expenses) + +Tagging criteria are evaluated during import and update operations. Operations can have multiple tags. + +#### 3. Transfer Detection Criteria + +Define rules to automatically detect transfers between accounts. Each criterion includes: + +- **Criterion Expression**: Binary predicate that matches a source (withdraw) and sink (income) operation + - Example: `(source, sink) => source.Amount.Amount == -sink.Amount.Amount && source.Timestamp.Date == sink.Timestamp.Date` +- **Accuracy**: Confidence level of the match + - `Exact` (100%) - High confidence, exact match + - `Likely` (70%) - Probable match, may require review +- **Comment**: Description for the transfer + +When a transfer is detected, both operations are tagged with `Transfer`, `Source`, or `Sink` tags and excluded from income/expense calculations. + +### Aggregation rules (Logbook Criteria) + +Logbook criteria define how operations are grouped and aggregated for expense tracking. Criteria form a hierarchical structure where each level can have subcriteria for further grouping. + +#### Criterion Types + +1. **Tag-Based Criterion** + - Matches operations by tags + - Types: + - `Including`: Operation must have ALL specified tags + - `OneOf`: Operation must have AT LEAST ONE of the specified tags + - `Excluding`: Operation must NOT have any of the specified tags + - Example: Group all operations tagged with "Food" or "Restaurant" + +2. **Predicate-Based Criterion** + - Matches operations using a boolean expression + - Example: `o => o.Amount.Amount < 0 && o.Timestamp.Month == DateTime.Now.Month` + - Useful for complex filtering logic + +3. **Substitution-Based Criterion** + - Groups operations by a computed value + - The substitution expression returns a string that becomes the group name + - Example: `o => o.Timestamp.ToString("yyyy-MM")` groups by month + - Automatically creates subcriteria for each unique value + +4. **Universal Criterion** + - Matches all operations + - Can be used as a root criterion or with subcriteria for grouping + - When used with subcriteria, operations are distributed to matching subcriteria + +#### Hierarchical Structure + +Criteria can be nested to create multi-level groupings: + +``` +Universal (all operations) +├── Tag-Based: "Food" (food-related expenses) +│ ├── Substitution: Month (group by month) +│ └── Tag-Based: "Restaurant" (restaurant expenses) +└── Tag-Based: "Transport" (transportation) + └── Substitution: Month +``` + +This structure allows you to build calendar-like views where operations are grouped by category and time period. + +### Import + +Import operations from CSV files exported by your banks. The import process uses the file reading settings configured for your budget to parse the CSV format. + +#### Prerequisites + +Before importing, ensure you have: +1. **Configured file reading settings** for your budget (see [Budget settings](#budget-settings)) +2. A CSV file exported from your bank + +#### Import Process + +1. **Navigate to the import page** for your budget +2. **Select a CSV file** to import +3. **Optional: Specify file pattern** - If you have multiple reading settings, provide a regex pattern to match the correct one (e.g., `.*sberbank.*\.csv`) +4. **Optional: Set transfer confidence level** - Choose the minimum accuracy for automatic transfer detection: + - `Exact` (100%) - Only detect transfers with high confidence + - `Likely` (70%) - Also detect probable transfers (may require review) +5. **Start the import** - The system will: + - Parse the CSV file using the matching reading settings + - Register new operations + - Apply tagging criteria automatically + - Detect transfers based on your transfer criteria + - Detect duplicate operations + +#### Import Results + +After import, you'll receive a summary with: + +- **Registered Operations**: New operations successfully imported +- **Duplicates**: Groups of operations that appear to be duplicates (same amount, timestamp, and description) +- **Errors**: Any parsing errors or validation failures +- **Success Messages**: Information about transfers detected, tags applied, etc. + +#### Handling Duplicates + +The system automatically detects potential duplicates based on: +- Same amount (absolute value) +- Same timestamp (within a small time window) +- Same description + +Duplicate groups are shown in the import results. You can: +- Review each duplicate group +- Keep the operations if they're legitimate (not actual duplicates) +- Delete duplicates manually after import + +#### Transfer Detection During Import + +If transfer detection criteria are configured, the system will automatically: +- Match withdraw operations with income operations +- Tag matched operations as `Transfer`, `Source`, or `Sink` +- Exclude transfers from income/expense calculations + +The transfer confidence level you select determines which transfers are automatically detected. You can review and adjust transfers later if needed. + +### Operations editing + +Edit operations individually or in bulk to correct data, add tags, or update attributes. + +#### Editing Individual Operations + +1. **Find the operation** in the operations list +2. **Click the edit button** (✏️) to enter edit mode +3. **Modify the fields**: + - **Description**: Transaction description + - **Amount**: Transaction amount (positive for income, negative for expenses) + - **Currency**: Currency code (e.g., USD, EUR, RUB) + - **Tags**: Add, remove, or modify tags + - **Attributes**: Add, remove, or modify custom attributes (key-value pairs) +4. **Save changes** (💾) or **Cancel** (✕) + +#### Editable Fields + +- **Description** (`string`): Free-form text describing the transaction +- **Amount** (`decimal`): Transaction amount with decimal precision +- **Currency Code** (`string`): ISO currency code (3 letters, e.g., USD, EUR) +- **Timestamp** (`DateTime`): Date and time of the transaction +- **Tags** (`string[]`): Array of tag names +- **Attributes** (`Dictionary`): Custom key-value pairs for additional metadata + +#### Bulk Updates + +You can update multiple operations at once by sending a batch update request. The update process will: +- Apply changes to all specified operations +- Re-evaluate tagging criteria (based on tagging mode) +- Re-detect transfers (if transfer confidence level is specified) +- Update operation versions for optimistic concurrency + +#### Tagging Modes + +When updating operations, you can choose how tags are handled: + +- **Append**: Add new tags from tagging criteria without removing existing tags +- **Replace**: Replace all tags with those from tagging criteria +- **None**: Don't apply tagging criteria (keep existing tags) + +#### Transfer Re-detection + +When updating operations, you can optionally re-run transfer detection: +- Specify a transfer confidence level (`Exact` or `Likely`) +- The system will re-evaluate all operations for potential transfers +- Previously detected transfers may be updated or removed if criteria no longer match + +#### Operation Details + +Each operation displays: +- **Operation ID**: Unique identifier +- **Budget ID**: The budget this operation belongs to +- **Version**: Version number for optimistic concurrency control + +You can expand an operation to view these details. + +#### Deleting Operations + +Operations can be deleted individually: +1. Click the delete button (🗑️) on the operation +2. Confirm the deletion + +**Note**: Deleted operations are permanently removed and cannot be recovered. Consider exporting your data before bulk deletions. + +### Logbook diff --git a/docs/console-commands.md b/docs/console-commands.md deleted file mode 100644 index c088f3d0..00000000 --- a/docs/console-commands.md +++ /dev/null @@ -1,22 +0,0 @@ -* budget - * acc - accounts - * stats - accounts-based statistics - * merge - merges two accounts into one by moving operation from one account to another - * ops (o) - operations - * list - list operations - * list-duplicates - get list of duplicated operations - * import - perform import - * update - update operations from file - * retag - update the list of tags - * remove - remove operations - * owners - owners - * list - list owners - * self-register - register current user as owner - * xfers (x) - transfers - * register - manually register transfers - * remove - removes registered transfers by source ids - * admin - administrative actions - * settings - list effective settings - * migrate-db - apply db migrations - * test - tests for configuration - * import - test csv reading options for particular file diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json new file mode 100644 index 00000000..26ae7bb6 --- /dev/null +++ b/src/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-aspnet-codegenerator": { + "version": "9.0.0", + "commands": [ + "dotnet-aspnet-codegenerator" + ] + }, + "dotnet-ef": { + "version": "9.0.2", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 00000000..cd967fc3 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json new file mode 100644 index 00000000..c84a92e1 --- /dev/null +++ b/src/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Hosts/NVs.Budget.Hosts.Web.Server/bin/Debug/net8.0/NVs.Budget.Hosts.Web.Server.dll", + "args": [], + "cwd": "${workspaceFolder}/Hosts/NVs.Budget.Hosts.Web.Server", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json new file mode 100644 index 00000000..e1574fa5 --- /dev/null +++ b/src/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/NVs.Budget.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/NVs.Budget.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/NVs.Budget.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/Application/NVs.Budget.Application.Contracts/Criteria/TaggingCriterion.cs b/src/Application/NVs.Budget.Application.Contracts/Criteria/TaggingCriterion.cs index ef14cce4..107e4976 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Criteria/TaggingCriterion.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Criteria/TaggingCriterion.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Utilities.Expressions; namespace NVs.Budget.Application.Contracts.Criteria; diff --git a/src/Application/NVs.Budget.Application.Contracts/Criteria/TransferCriterion.cs b/src/Application/NVs.Budget.Application.Contracts/Criteria/TransferCriterion.cs index a8295d47..1ee22eb6 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Criteria/TransferCriterion.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Criteria/TransferCriterion.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Utilities.Expressions; namespace NVs.Budget.Application.Contracts.Criteria; diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/DetectionAccuracy.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/DetectionAccuracy.cs index c8e4a98d..9c211ec8 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/DetectionAccuracy.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/DetectionAccuracy.cs @@ -1,4 +1,4 @@ -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public enum DetectionAccuracy { diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedBudget.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedBudget.cs index 643479c1..ca97e422 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedBudget.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedBudget.cs @@ -1,10 +1,10 @@ using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public class TrackedBudget(Guid id, string name, IEnumerable owners, IEnumerable taggingCriteria, IEnumerable transferCriteria, LogbookCriteria logbookCriteria) - : Domain.Entities.Accounts.Budget(id, name, owners), ITrackableEntity + : Domain.Entities.Budgets.Budget(id, name, owners), ITrackableEntity { private readonly List _taggingCriteria = [..Order(taggingCriteria)]; private readonly List _transferCriteria = [..Order(transferCriteria)]; diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOperation.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOperation.cs index 8ddeb326..2a1c0d2d 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOperation.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOperation.cs @@ -2,14 +2,14 @@ using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public class TrackedOperation( Guid id, DateTime timestamp, Money amount, string description, - Domain.Entities.Accounts.Budget budget, + Domain.Entities.Budgets.Budget budget, IEnumerable tags, IReadOnlyDictionary? attributes) : Operation(id, timestamp, amount, description, budget, tags, attributes), ITrackableEntity diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOwner.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOwner.cs index a4a66c47..965a60b0 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOwner.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedOwner.cs @@ -1,6 +1,6 @@ -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public class TrackedOwner(Guid id, string name) : Owner(id, name), ITrackableEntity { diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedTransfer.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedTransfer.cs index 41c0b5dc..1ca9c7bf 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedTransfer.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TrackedTransfer.cs @@ -2,7 +2,7 @@ using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.Entities.Transactions; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public class TrackedTransfer : Transfer { diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransferTags.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransferTags.cs index 393e82e4..ae350ae6 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransferTags.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransferTags.cs @@ -1,7 +1,8 @@ -using NVs.Budget.Domain.Entities.Operations; +using System.Diagnostics.CodeAnalysis; +using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; -namespace NVs.Budget.Application.Services.Accounting.Transfers; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public static class TransferTags { @@ -10,6 +11,25 @@ public static class TransferTags public static readonly Tag Sink = new(nameof(Domain.Entities.Transactions.Transfer.Sink)); public static readonly Tag Ephemeral = new(nameof(Ephemeral)); + private static Tag[]? _tags; + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "it's a static class, all private fields looks better with underscore")] + private static readonly object _tagsLock = new (); + public static Tag[] All + { + get + { + if (_tags is null) + { + lock (_tagsLock) + { + _tags ??= [Transfer, Source, Sink, Ephemeral]; + } + } + + return _tags; + } + } + public static T TagSource(this T operation) where T:Operation { operation.Tag(Transfer); @@ -20,10 +40,7 @@ public static T TagSource(this T operation) where T:Operation public static T TagEphemeral(this T operation) where T:Operation { - operation.Tag(Transfer); operation.Tag(Ephemeral); - operation.Tag(Source); - operation.Tag(Sink); return operation; } diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransfersList.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransfersList.cs new file mode 100644 index 00000000..1a8de2a5 --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/TransfersList.cs @@ -0,0 +1,20 @@ +namespace NVs.Budget.Application.Contracts.Entities.Accounting; + +public sealed class TransfersList +{ + private readonly List _recorded = new(); + private readonly List _unregistered = new(); + + public IReadOnlyCollection Recorded => _recorded.AsReadOnly(); + public IReadOnlyCollection Unregistered => _unregistered.AsReadOnly(); + + public void Add(TrackedTransfer transfer) + { + _recorded.Add(transfer); + } + + public void Add(UnregisteredTransfer transfer) + { + _unregistered.Add(transfer); + } +} \ No newline at end of file diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredBudget.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredBudget.cs index 086035b4..7e55c8ac 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredBudget.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredBudget.cs @@ -1,3 +1,3 @@ -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public record UnregisteredBudget(string Name); diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredOperation.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredOperation.cs index 6cea9349..e794298b 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredOperation.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredOperation.cs @@ -1,6 +1,6 @@ using NMoneys; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public record UnregisteredOperation( DateTime Timestamp, diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredTransfer.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredTransfer.cs index f69bf409..57371504 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredTransfer.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/Accounting/UnregisteredTransfer.cs @@ -1,5 +1,5 @@ using NMoneys; -namespace NVs.Budget.Application.Contracts.Entities.Budgeting; +namespace NVs.Budget.Application.Contracts.Entities.Accounting; public record UnregisteredTransfer(TrackedOperation Source, TrackedOperation Sink, Money Fee, string Comment, DetectionAccuracy Accuracy); diff --git a/src/Application/NVs.Budget.Application.Contracts/Entities/IUser.cs b/src/Application/NVs.Budget.Application.Contracts/Entities/IUser.cs index c76e26eb..cbf028c0 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Entities/IUser.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Entities/IUser.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; namespace NVs.Budget.Application.Contracts.Entities; diff --git a/src/Application/NVs.Budget.Application.Contracts/Options/ImportOptions.cs b/src/Application/NVs.Budget.Application.Contracts/Options/ImportOptions.cs index f8b1109f..e83c3aac 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Options/ImportOptions.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Options/ImportOptions.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.Options; diff --git a/src/Application/NVs.Budget.Application.Contracts/Options/UpdateOptions.cs b/src/Application/NVs.Budget.Application.Contracts/Options/UpdateOptions.cs index a4f3721d..a8268407 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Options/UpdateOptions.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Options/UpdateOptions.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.Options; diff --git a/src/Application/NVs.Budget.Application.Contracts/Queries/LogbookQuery.cs b/src/Application/NVs.Budget.Application.Contracts/Queries/LogbookQuery.cs index 3fd6c247..a64e3e0b 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Queries/LogbookQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Queries/LogbookQuery.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.ValueObjects.Criteria; namespace NVs.Budget.Application.Contracts.Queries; diff --git a/src/Application/NVs.Budget.Application.Contracts/Queries/OperationQuery.cs b/src/Application/NVs.Budget.Application.Contracts/Queries/OperationQuery.cs index a13c61ce..1854d189 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Queries/OperationQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Queries/OperationQuery.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.Queries; diff --git a/src/Application/NVs.Budget.Application.Contracts/Results/ImportResult.cs b/src/Application/NVs.Budget.Application.Contracts/Results/ImportResult.cs index 73b7e5da..96496df9 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Results/ImportResult.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Results/ImportResult.cs @@ -1,5 +1,5 @@ using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.Results; diff --git a/src/Application/NVs.Budget.Application.Contracts/Results/UpdateResult.cs b/src/Application/NVs.Budget.Application.Contracts/Results/UpdateResult.cs index ae095edd..b20133e0 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Results/UpdateResult.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Results/UpdateResult.cs @@ -1,5 +1,5 @@ using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.Results; diff --git a/src/Application/NVs.Budget.Application.Contracts/Services/IAccountant.cs b/src/Application/NVs.Budget.Application.Contracts/Services/IAccountant.cs index b3a67ade..b163efb4 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Services/IAccountant.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Services/IAccountant.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Results; @@ -11,5 +11,7 @@ public interface IAccountant Task ImportOperations(IAsyncEnumerable unregistered, TrackedBudget budget, ImportOptions options, CancellationToken ct); Task Update(IAsyncEnumerable operations, TrackedBudget budget, UpdateOptions options, CancellationToken ct); Task Remove(Expression> criteria, CancellationToken ct); + Task GetTransfers(DateTime from, DateTime till, TrackedBudget budget, CancellationToken ct); Task RegisterTransfers(IAsyncEnumerable transfers, CancellationToken ct); + Task RemoveTransfers(IAsyncEnumerable transfers, CancellationToken ct); } diff --git a/src/Application/NVs.Budget.Application.Contracts/Services/IBudgetManager.cs b/src/Application/NVs.Budget.Application.Contracts/Services/IBudgetManager.cs index bcbd18b1..4ee55106 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Services/IBudgetManager.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Services/IBudgetManager.cs @@ -1,6 +1,6 @@ using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; namespace NVs.Budget.Application.Contracts.Services; diff --git a/src/Application/NVs.Budget.Application.Contracts/Services/IReckoner.cs b/src/Application/NVs.Budget.Application.Contracts/Services/IReckoner.cs index c2f03f89..7dbce90a 100644 --- a/src/Application/NVs.Budget.Application.Contracts/Services/IReckoner.cs +++ b/src/Application/NVs.Budget.Application.Contracts/Services/IReckoner.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Queries; using NVs.Budget.Domain.Aggregates; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/ListOwnedBudgetsQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/ListOwnedBudgetsQuery.cs deleted file mode 100644 index 64d98312..00000000 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/ListOwnedBudgetsQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Application.Contracts.UseCases.Accounts; - -public class ListOwnedBudgetsQuery : IRequest>; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/MergeAccountsRequest.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/MergeAccountsRequest.cs deleted file mode 100644 index 97f21e94..00000000 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Accounts/MergeAccountsRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -using FluentResults; -using MediatR; - -namespace NVs.Budget.Application.Contracts.UseCases.Accounts; - -public record MergeAccountsRequest(IReadOnlyList BudgetIds, bool PurgeEmptyBudgets) : IRequest; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ChangeBudgetOwnersCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ChangeBudgetOwnersCommand.cs new file mode 100644 index 00000000..7c3d83f3 --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ChangeBudgetOwnersCommand.cs @@ -0,0 +1,8 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public record ChangeBudgetOwnersCommand(TrackedBudget Budget, IReadOnlyCollection Owners) : IRequest; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ListOwnedBudgetsQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ListOwnedBudgetsQuery.cs new file mode 100644 index 00000000..74faf820 --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/ListOwnedBudgetsQuery.cs @@ -0,0 +1,6 @@ +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public class ListOwnedBudgetsQuery : IRequest>; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/MergeBudgetsRequest.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/MergeBudgetsRequest.cs new file mode 100644 index 00000000..079dde14 --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/MergeBudgetsRequest.cs @@ -0,0 +1,6 @@ +using FluentResults; +using MediatR; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public record MergeBudgetsRequest(IReadOnlyList BudgetIds, bool PurgeEmptyBudgets) : IRequest; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RegisterBudgetCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RegisterBudgetCommand.cs new file mode 100644 index 00000000..0bb1c70f --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RegisterBudgetCommand.cs @@ -0,0 +1,7 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public record RegisterBudgetCommand(UnregisteredBudget NewBudget) : IRequest>; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RemoveBudgetCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RemoveBudgetCommand.cs new file mode 100644 index 00000000..324b7fdc --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/RemoveBudgetCommand.cs @@ -0,0 +1,7 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public record RemoveBudgetCommand(TrackedBudget Budget) : IRequest; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/UpdateBudgetCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/UpdateBudgetCommand.cs new file mode 100644 index 00000000..d06db7a4 --- /dev/null +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Budgets/UpdateBudgetCommand.cs @@ -0,0 +1,7 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Application.Contracts.UseCases.Budgets; + +public record UpdateBudgetCommand(TrackedBudget Budget) : IRequest; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/CalcOperationsStatisticsQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/CalcOperationsStatisticsQuery.cs index 6a859e82..dd983c40 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/CalcOperationsStatisticsQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/CalcOperationsStatisticsQuery.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.Aggregates; using NVs.Budget.Domain.ValueObjects.Criteria; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ImportOperationsCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ImportOperationsCommand.cs index 97b1ddec..13667dcf 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ImportOperationsCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ImportOperationsCommand.cs @@ -1,5 +1,5 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Results; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListDuplicatedOperationsQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListDuplicatedOperationsQuery.cs index 377659b4..e66189b5 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListDuplicatedOperationsQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListDuplicatedOperationsQuery.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListOperationsQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListOperationsQuery.cs index b3d79da0..368067bd 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListOperationsQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/ListOperationsQuery.cs @@ -1,5 +1,5 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Queries; namespace NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RemoveOperationsCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RemoveOperationsCommand.cs index 9671ad10..2050e33a 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RemoveOperationsCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RemoveOperationsCommand.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RetagOperationsCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RetagOperationsCommand.cs index 09d28f82..88e73b9c 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RetagOperationsCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/RetagOperationsCommand.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/UpdateOperationsCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/UpdateOperationsCommand.cs index bf20312b..1166b545 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/UpdateOperationsCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Operations/UpdateOperationsCommand.cs @@ -1,6 +1,5 @@ -using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Results; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/ListOwnersQuery.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/ListOwnersQuery.cs index 4543989e..9c5df388 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/ListOwnersQuery.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/ListOwnersQuery.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Owners; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/RegisterOwnerCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/RegisterOwnerCommand.cs index e481e82c..9bc326ae 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/RegisterOwnerCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Owners/RegisterOwnerCommand.cs @@ -1,7 +1,7 @@ using FluentResults; using MediatR; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Owners; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/RegisterTransfersCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/RegisterTransfersCommand.cs index 03e4cdd3..82428e12 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/RegisterTransfersCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/RegisterTransfersCommand.cs @@ -1,6 +1,6 @@ using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Transfers; diff --git a/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/SearchTransfersCommand.cs b/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/SearchTransfersCommand.cs index eaac072f..2fd394e8 100644 --- a/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/SearchTransfersCommand.cs +++ b/src/Application/NVs.Budget.Application.Contracts/UseCases/Transfers/SearchTransfersCommand.cs @@ -1,9 +1,7 @@ using System.Linq.Expressions; -using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Transactions; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Contracts.UseCases.Transfers; -public record SearchTransfersCommand(TrackedBudget Budget, Expression> Criteria, DetectionAccuracy? Accuracy): IRequest>; +public record SearchTransfersCommand(TrackedBudget Budget, DateTime From, DateTime Till, DetectionAccuracy? Accuracy): IRequest; diff --git a/src/Application/NVs.Budget.Application.Tests/AccountantShould.cs b/src/Application/NVs.Budget.Application.Tests/AccountantShould.cs index 43c93b6b..e7f3968b 100644 --- a/src/Application/NVs.Budget.Application.Tests/AccountantShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/AccountantShould.cs @@ -2,13 +2,12 @@ using Moq; using NVs.Budget.Application.Contracts.Criteria; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Services.Accounting; using NVs.Budget.Application.Services.Accounting.Duplicates; -using NVs.Budget.Application.Services.Accounting.Results; using NVs.Budget.Application.Tests.Fakes; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Utilities.Expressions; using NVs.Budget.Utilities.Testing; diff --git a/src/Application/NVs.Budget.Application.Tests/BudgetManagerShould.cs b/src/Application/NVs.Budget.Application.Tests/BudgetManagerShould.cs index 8141330f..954fd74e 100644 --- a/src/Application/NVs.Budget.Application.Tests/BudgetManagerShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/BudgetManagerShould.cs @@ -3,12 +3,12 @@ using Moq; using NVs.Budget.Application.Contracts.Criteria; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Errors.Accounting; using NVs.Budget.Application.Services.Accounting; using NVs.Budget.Application.Services.Accounting.Results.Errors; using NVs.Budget.Application.Tests.Fakes; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Utilities.Testing; namespace NVs.Budget.Application.Tests; @@ -32,23 +32,23 @@ public BudgetManagerShould() } [Fact] - public async Task ReturnOnlyOwnedAccounts() + public async Task ReturnOnlyOwnedBudgets() { - var ownedAccounts = GenerateBudgets(3, _owner).ToList(); - var notOwnedAccounts = GenerateBudgets(3, _fixture.Create()); + var ownedBudgets = GenerateBudgets(3, _owner).ToList(); + var notOwnedBudgets = GenerateBudgets(3, _fixture.Create()); - _repository.Append(ownedAccounts); - _repository.Append(notOwnedAccounts); + _repository.Append(ownedBudgets); + _repository.Append(notOwnedBudgets); var budgets = await _manager.GetOwnedBudgets(CancellationToken.None); - budgets.Should().BeEquivalentTo(ownedAccounts); + budgets.Should().BeEquivalentTo(ownedBudgets); } [Fact] - public async Task CreateAccount() + public async Task CreateBudget() { - var newAccount = _fixture.Create(); - var result = await _manager.Register(newAccount, CancellationToken.None); + var newBudget = _fixture.Create(); + var result = await _manager.Register(newBudget, CancellationToken.None); result.IsSuccess.Should().BeTrue(); result.Value.Owners.Should().HaveCount(1); @@ -59,7 +59,7 @@ public async Task CreateAccount() } [Fact] - public async Task RenameOwnedAccounts() + public async Task RenameOwnedBudgets() { IReadOnlyList getOwnedBudgets = GenerateBudgets(1, _owner).ToList().AsReadOnly(); _repository.Append(getOwnedBudgets); @@ -85,7 +85,7 @@ public async Task RenameOwnedAccounts() } [Fact] - public async Task NotUpdateAccountThatDoesNotBelongToCurrentOwner() + public async Task NotUpdateBudgetThatDoesNotBelongToCurrentOwner() { var budgets = GenerateBudgets(1, _fixture.Create()).ToList(); _repository.Append(budgets); @@ -97,7 +97,7 @@ public async Task NotUpdateAccountThatDoesNotBelongToCurrentOwner() } [Fact] - public async Task NotUpdateAccountThatDoesNotExists() + public async Task NotUpdateBudgetThatDoesNotExists() { var result = await _manager.Update(_fixture.Create(), CancellationToken.None); result.IsSuccess.Should().BeFalse(); @@ -138,7 +138,7 @@ public async Task NotChangeOwnersIfCurrentOwnerLosesAccess() } [Fact] - public async Task RemoveAccountOwnedOnlyByCurrentOwner() + public async Task RemoveBudgetOwnedOnlyByCurrentOwner() { IReadOnlyList budgets = GenerateBudgets(1, _owner).ToList().AsReadOnly(); _repository.Append(budgets); @@ -153,7 +153,7 @@ public async Task RemoveAccountOwnedOnlyByCurrentOwner() } [Fact] - public async Task NotRemoveAccountOwnedByMultipleOwners() + public async Task NotRemoveBudgetOwnedByMultipleOwners() { var budgets = GenerateBudgets(1, _fixture.Create(), _owner).ToList(); _repository.Append(budgets); diff --git a/src/Application/NVs.Budget.Application.Tests/DuplicatesDetectorShould.cs b/src/Application/NVs.Budget.Application.Tests/DuplicatesDetectorShould.cs index 98219e90..1fd4137c 100644 --- a/src/Application/NVs.Budget.Application.Tests/DuplicatesDetectorShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/DuplicatesDetectorShould.cs @@ -1,6 +1,6 @@ using AutoFixture; using FluentAssertions; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Services.Accounting.Duplicates; diff --git a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeBudgetsRepository.cs b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeBudgetsRepository.cs index 96c7ee62..7fad537e 100644 --- a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeBudgetsRepository.cs +++ b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeBudgetsRepository.cs @@ -1,7 +1,7 @@ using FluentResults; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; namespace NVs.Budget.Application.Tests.Fakes; diff --git a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeOperationsRepository.cs b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeOperationsRepository.cs index 23249133..42460a02 100644 --- a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeOperationsRepository.cs +++ b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeOperationsRepository.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeTransfersRepository.cs b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeTransfersRepository.cs index 125816ab..851bf293 100644 --- a/src/Application/NVs.Budget.Application.Tests/Fakes/FakeTransfersRepository.cs +++ b/src/Application/NVs.Budget.Application.Tests/Fakes/FakeTransfersRepository.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; namespace NVs.Budget.Application.Tests.Fakes; diff --git a/src/Application/NVs.Budget.Application.Tests/ImportTestData.cs b/src/Application/NVs.Budget.Application.Tests/ImportTestData.cs index 2c72c70d..9a3bc1b3 100644 --- a/src/Application/NVs.Budget.Application.Tests/ImportTestData.cs +++ b/src/Application/NVs.Budget.Application.Tests/ImportTestData.cs @@ -1,7 +1,7 @@ using AutoFixture; using FluentAssertions; using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Results; using NVs.Budget.Utilities.Testing; diff --git a/src/Application/NVs.Budget.Application.Tests/MoneyConverterShould.cs b/src/Application/NVs.Budget.Application.Tests/MoneyConverterShould.cs index b8a9b55f..54a83e78 100644 --- a/src/Application/NVs.Budget.Application.Tests/MoneyConverterShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/MoneyConverterShould.cs @@ -4,7 +4,7 @@ using NMoneys; using NVs.Budget.Application.Contracts.Entities; using NVs.Budget.Application.Services.Accounting.Exchange; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.ExchangeRates.Contracts; diff --git a/src/Application/NVs.Budget.Application.Tests/NVs.Budget.Application.Tests.csproj b/src/Application/NVs.Budget.Application.Tests/NVs.Budget.Application.Tests.csproj index 3013a850..19e27640 100644 --- a/src/Application/NVs.Budget.Application.Tests/NVs.Budget.Application.Tests.csproj +++ b/src/Application/NVs.Budget.Application.Tests/NVs.Budget.Application.Tests.csproj @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Application/NVs.Budget.Application.Tests/ReckonerShould.cs b/src/Application/NVs.Budget.Application.Tests/ReckonerShould.cs index d93dd46f..a97cab5d 100644 --- a/src/Application/NVs.Budget.Application.Tests/ReckonerShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/ReckonerShould.cs @@ -1,13 +1,11 @@ -using System.Collections; -using System.Collections.ObjectModel; -using AutoFixture; +using AutoFixture; using FluentAssertions; using FluentAssertions.Execution; using Moq; using NMoneys; using NVs.Budget.Application.Contracts.Criteria; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Queries; using NVs.Budget.Application.Services.Accounting; @@ -16,7 +14,7 @@ using NVs.Budget.Application.Services.Accounting.Reckon; using NVs.Budget.Application.Services.Accounting.Transfers; using NVs.Budget.Application.Tests.Fakes; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Domain.ValueObjects.Criteria; @@ -55,7 +53,7 @@ public ReckonerShould() manager); _data = new ReckonerTestData(_currentOwner, 2, 4, 6); - _storage.Budgets.Append(_data.AllAccounts); + _storage.Budgets.Append(_data.AllBudgets); _storage.Operations.Append(_data.AllTransactions); } diff --git a/src/Application/NVs.Budget.Application.Tests/ReckonerTestData.cs b/src/Application/NVs.Budget.Application.Tests/ReckonerTestData.cs index 204eae49..37663425 100644 --- a/src/Application/NVs.Budget.Application.Tests/ReckonerTestData.cs +++ b/src/Application/NVs.Budget.Application.Tests/ReckonerTestData.cs @@ -1,7 +1,7 @@ using AutoFixture; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Utilities.Testing; namespace NVs.Budget.Application.Tests; @@ -14,7 +14,7 @@ internal class ReckonerTestData public IReadOnlyList NotOwnedTransactions { get; } - public IEnumerable AllAccounts => OwnedBudgets + public IEnumerable AllBudgets => OwnedBudgets .Concat(OwnedTransactions.Select(t => t.Budget as TrackedBudget)) .Concat(NotOwnedTransactions.Select(t => t.Budget as TrackedBudget)) .Where(a => a is not null) @@ -22,13 +22,13 @@ internal class ReckonerTestData public IEnumerable AllTransactions => OwnedTransactions.Concat(NotOwnedTransactions); - public ReckonerTestData(Owner owner, int ownedAccountsCount = 2, int ownedTransactionsPerAccount = 3, int notOwnedTransactionsCount = 5) + public ReckonerTestData(Owner owner, int ownedBudgetsCount = 2, int ownedTransactionsPerBudget = 3, int notOwnedTransactionsCount = 5) { var fixture = new Fixture() { Customizations = { new ReadableExpressionsBuilder() }}; fixture.Inject(LogbookCriteria.Universal); OwnedBudgets = fixture .CreateMany() - .Take(ownedAccountsCount) + .Take(ownedBudgetsCount) .ToList(); foreach (var budget in OwnedBudgets) { @@ -37,11 +37,11 @@ public ReckonerTestData(Owner owner, int ownedAccountsCount = 2, int ownedTransa OwnedTransactions = OwnedBudgets.SelectMany((a, i) => { - using (fixture.SetAccount(a)) + using (fixture.SetBudget(a)) { return i % 2 == 0 - ? fixture.CreateWithdraws(ownedTransactionsPerAccount) - : fixture.CreateIncomes(ownedTransactionsPerAccount); + ? fixture.CreateWithdraws(ownedTransactionsPerBudget) + : fixture.CreateIncomes(ownedTransactionsPerBudget); } }).ToList(); diff --git a/src/Application/NVs.Budget.Application.Tests/TagsManagerShould.cs b/src/Application/NVs.Budget.Application.Tests/TagsManagerShould.cs index a595aab3..76175ce9 100644 --- a/src/Application/NVs.Budget.Application.Tests/TagsManagerShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/TagsManagerShould.cs @@ -1,7 +1,7 @@ using AutoFixture; using FluentAssertions; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Services.Accounting.Tags; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Utilities.Expressions; diff --git a/src/Application/NVs.Budget.Application.Tests/TransferDetectorShould.cs b/src/Application/NVs.Budget.Application.Tests/TransferDetectorShould.cs index 0917826b..aea68327 100644 --- a/src/Application/NVs.Budget.Application.Tests/TransferDetectorShould.cs +++ b/src/Application/NVs.Budget.Application.Tests/TransferDetectorShould.cs @@ -2,7 +2,7 @@ using FluentAssertions; using NMoneys; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Services.Accounting.Transfers; using NVs.Budget.Utilities.Expressions; using NVs.Budget.Utilities.Testing; diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/ChangeBudgetOwnersCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/ChangeBudgetOwnersCommandHandler.cs new file mode 100644 index 00000000..016514b3 --- /dev/null +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/ChangeBudgetOwnersCommandHandler.cs @@ -0,0 +1,12 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Services; +using NVs.Budget.Application.Contracts.UseCases.Budgets; + +namespace NVs.Budget.Application.UseCases.Budgets; + +internal class ChangeBudgetOwnersCommandHandler(IBudgetManager manager) : IRequestHandler +{ + public Task Handle(ChangeBudgetOwnersCommand request, CancellationToken cancellationToken) => + manager.ChangeOwners(request.Budget, request.Owners, cancellationToken); +} diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/ListOwnedBudgetsQueryHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/ListOwnedBudgetsQueryHandler.cs index b2a72658..1bdcdd3c 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Budgets/ListOwnedBudgetsQueryHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/ListOwnedBudgetsQueryHandler.cs @@ -1,9 +1,9 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Accounts; +using NVs.Budget.Application.Contracts.UseCases.Budgets; -namespace NVs.Budget.Application.UseCases.Accounts; +namespace NVs.Budget.Application.UseCases.Budgets; internal class ListOwnedBudgetsQueryHandler(IBudgetManager manager) : IRequestHandler> { diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/MergeAccountsRequestHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/MergeBudgetsRequestHandler.cs similarity index 78% rename from src/Application/NVs.Budget.Application.UseCases/Budgets/MergeAccountsRequestHandler.cs rename to src/Application/NVs.Budget.Application.UseCases/Budgets/MergeBudgetsRequestHandler.cs index de6ce618..4231cc98 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Budgets/MergeAccountsRequestHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/MergeBudgetsRequestHandler.cs @@ -1,16 +1,16 @@ using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Errors.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Accounts; +using NVs.Budget.Application.Contracts.UseCases.Budgets; -namespace NVs.Budget.Application.UseCases.Accounts; +namespace NVs.Budget.Application.UseCases.Budgets; -internal class MergeAccountsRequestHandler(IBudgetManager manager, IReckoner reckoner, IAccountant accountant) : IRequestHandler +internal class MergeBudgetsRequestHandler(IBudgetManager manager, IReckoner reckoner, IAccountant accountant) : IRequestHandler { - public async Task Handle(MergeAccountsRequest request, CancellationToken cancellationToken) + public async Task Handle(MergeBudgetsRequest request, CancellationToken cancellationToken) { var budgets = (await manager.GetOwnedBudgets(cancellationToken)).Where(b => request.BudgetIds.Contains(b.Id)).ToDictionary(x => x.Id); var missedBudgets = request.BudgetIds.Except(budgets.Keys).ToList(); diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/RegisterBudgetCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/RegisterBudgetCommandHandler.cs new file mode 100644 index 00000000..86e9af4f --- /dev/null +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/RegisterBudgetCommandHandler.cs @@ -0,0 +1,13 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Application.Contracts.Services; +using NVs.Budget.Application.Contracts.UseCases.Budgets; + +namespace NVs.Budget.Application.UseCases.Budgets; + +internal class RegisterBudgetCommandHandler(IBudgetManager manager) : IRequestHandler> +{ + public Task> Handle(RegisterBudgetCommand request, CancellationToken cancellationToken) => + manager.Register(request.NewBudget, cancellationToken); +} diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/RemoveBudgetCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/RemoveBudgetCommandHandler.cs new file mode 100644 index 00000000..4cc7ea3e --- /dev/null +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/RemoveBudgetCommandHandler.cs @@ -0,0 +1,12 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Services; +using NVs.Budget.Application.Contracts.UseCases.Budgets; + +namespace NVs.Budget.Application.UseCases.Budgets; + +internal class RemoveBudgetCommandHandler(IBudgetManager manager) : IRequestHandler +{ + public Task Handle(RemoveBudgetCommand request, CancellationToken cancellationToken) => + manager.Remove(request.Budget, cancellationToken); +} diff --git a/src/Application/NVs.Budget.Application.UseCases/Budgets/UpdateBudgetCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Budgets/UpdateBudgetCommandHandler.cs new file mode 100644 index 00000000..7434149f --- /dev/null +++ b/src/Application/NVs.Budget.Application.UseCases/Budgets/UpdateBudgetCommandHandler.cs @@ -0,0 +1,12 @@ +using FluentResults; +using MediatR; +using NVs.Budget.Application.Contracts.Services; +using NVs.Budget.Application.Contracts.UseCases.Budgets; + +namespace NVs.Budget.Application.UseCases.Budgets; + +internal class UpdateBudgetCommandHandler(IBudgetManager manager) : IRequestHandler +{ + public Task Handle(UpdateBudgetCommand request, CancellationToken cancellationToken) => + manager.Update(request.Budget, cancellationToken); +} diff --git a/src/Application/NVs.Budget.Application.UseCases/Operations/ListDuplicatedOperationsQueryHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Operations/ListDuplicatedOperationsQueryHandler.cs index 053730d2..cd6f9e08 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Operations/ListDuplicatedOperationsQueryHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Operations/ListDuplicatedOperationsQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.UseCases/Operations/ListOperationsQueryHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Operations/ListOperationsQueryHandler.cs index 10881a7d..0d3e66a5 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Operations/ListOperationsQueryHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Operations/ListOperationsQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Application.Contracts.UseCases.Operations; diff --git a/src/Application/NVs.Budget.Application.UseCases/Operations/UpdateOperationsCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Operations/UpdateOperationsCommandHandler.cs index aa869e61..d984bce0 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Operations/UpdateOperationsCommandHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Operations/UpdateOperationsCommandHandler.cs @@ -1,4 +1,3 @@ -using FluentResults; using MediatR; using NVs.Budget.Application.Contracts.Results; using NVs.Budget.Application.Contracts.Services; diff --git a/src/Application/NVs.Budget.Application.UseCases/Owners/ListOwnersQueryHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Owners/ListOwnersQueryHandler.cs index ba417e07..ef841787 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Owners/ListOwnersQueryHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Owners/ListOwnersQueryHandler.cs @@ -1,5 +1,5 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.UseCases.Owners; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Application.UseCases/Owners/RegisterOwnerHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Owners/RegisterOwnerHandler.cs index eb8484d0..f5f9dd70 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Owners/RegisterOwnerHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Owners/RegisterOwnerHandler.cs @@ -1,6 +1,6 @@ using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.UseCases.Owners; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Application.UseCases/Transfers/RemoveTransfersCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Transfers/RemoveTransfersCommandHandler.cs index eda6803b..76044e7c 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Transfers/RemoveTransfersCommandHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Transfers/RemoveTransfersCommandHandler.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.UseCases.Transfers; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Application.UseCases/Transfers/SearchTransfersCommandHandler.cs b/src/Application/NVs.Budget.Application.UseCases/Transfers/SearchTransfersCommandHandler.cs index 2845d403..88cc2238 100644 --- a/src/Application/NVs.Budget.Application.UseCases/Transfers/SearchTransfersCommandHandler.cs +++ b/src/Application/NVs.Budget.Application.UseCases/Transfers/SearchTransfersCommandHandler.cs @@ -1,21 +1,12 @@ using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Application.Contracts.UseCases.Transfers; namespace NVs.Budget.Application.UseCases.Transfers; -internal class SearchTransfersCommandHandler(IReckoner reckoner, IAccountant accountant) : IRequestHandler> +internal class SearchTransfersCommandHandler(IAccountant accountant) : IRequestHandler { - public async Task> Handle(SearchTransfersCommand request, CancellationToken cancellationToken) - { - var operations = reckoner.GetOperations(new(request.Criteria, null, true), cancellationToken); - //HACK: materializing operations to avoid "A command is already in progress" error using OrderBy - operations = operations.OrderBy(o => o.Timestamp) - .Where(o => o.IsRegistered); - - var results = await accountant.Update(operations, request.Budget, new(request.Accuracy, TaggingMode.Skip), cancellationToken); - return results.Transfers; - } + public Task Handle(SearchTransfersCommand request, CancellationToken cancellationToken) => accountant.GetTransfers(request.From, request.Till, request.Budget, cancellationToken); } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Accountant.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Accountant.cs index b4e7b09c..66181c2b 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Accountant.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Accountant.cs @@ -1,7 +1,8 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Application.Contracts.Errors.Accounting; using NVs.Budget.Application.Contracts.Options; using NVs.Budget.Application.Contracts.Results; using NVs.Budget.Application.Contracts.Services; @@ -51,8 +52,8 @@ public async Task Update(IAsyncEnumerable operat { if (budgets.Any(b => b.Id == o.Budget.Id)) return true; errors.Add(new BudgetDoesNotBelongToCurrentOwnerError() - .WithTransactionId(o) - .WithOperationId(o.Budget)); + .WithOperationId(o) + .WithBudgetId(o.Budget)); return false; }); @@ -65,7 +66,7 @@ public async Task Update(IAsyncEnumerable operat .Select(r => r.Value); - var transfers = GetTransfers(saved, budget, ct) + var transfers = GetTransfers(saved, budget, options.TransferConfidenceLevel, ct) .Select(t => resultBuilder.Append(t)) .Select(r => r.Value); @@ -94,7 +95,7 @@ private async IAsyncEnumerable Retag(IAsyncEnumerable Retag(IAsyncEnumerable GetTransfers(IAsyncEnumerable operations, TrackedBudget budget, [EnumeratorCancellation] CancellationToken ct) + private async IAsyncEnumerable GetTransfers(IAsyncEnumerable operations, TrackedBudget budget, DetectionAccuracy? tagIf, [EnumeratorCancellation] CancellationToken ct) { - var builder = new TransfersListBuilder(new TransferDetector(budget.TransferCriteria)); + var builder = new TransfersListBuilder(new TransferDetector(budget.TransferCriteria), tagIf); await foreach (var operation in operations.WithCancellation(ct)) { var transfer = builder.Add(operation); @@ -146,6 +147,34 @@ public async Task Remove(Expression> criter return result; } + public async Task GetTransfers(DateTime from, DateTime till, TrackedBudget budget, CancellationToken ct) + { + var registered = transfersRepository.Get( + t => t.StartedAt>=from || t.CompletedAt<=till, ct); + var result = new TransfersList(); + + await foreach (var transfer in registered.OrderByDescending(t => t.StartedAt).WithCancellation(ct)) + { + result.Add(transfer); + } + + var operations = streamingOperationRepository.Get( + o => o.Timestamp >= from && o.Timestamp <= till && o.Tags.All(t => t.Value != TransferTags.Transfer.Value), ct + ).OrderByDescending(t => t.Timestamp); + await foreach(var transfer in GetTransfers(operations, budget, null, ct)) + { + result.Add(new UnregisteredTransfer( + (TrackedOperation)transfer.Source, + (TrackedOperation)transfer.Sink, + transfer.Fee, + transfer.Comment, + transfer.Accuracy) + ); + } + + return result; + } + public async Task RegisterTransfers(IAsyncEnumerable transfers, CancellationToken ct) { var budgets = (await Manager.GetOwnedBudgets(ct)).Select(a => a.Id).ToList(); @@ -159,13 +188,13 @@ public async Task RegisterTransfers(IAsyncEnumerable a != transfer.Source.Budget.Id)) { - result.Reasons.Add(new BudgetDoesNotBelongToCurrentOwnerError().WithOperationId(transfer.Source.Budget)); + result.Reasons.Add(new BudgetDoesNotBelongToCurrentOwnerError().WithBudgetId(transfer.Source.Budget)); continue; } if (budgets.All(a => a != transfer.Sink.Budget.Id)) { - result.Reasons.Add(new BudgetDoesNotBelongToCurrentOwnerError().WithOperationId(transfer.Sink.Budget)); + result.Reasons.Add(new BudgetDoesNotBelongToCurrentOwnerError().WithBudgetId(transfer.Sink.Budget)); continue; } @@ -198,4 +227,55 @@ public async Task RegisterTransfers(IAsyncEnumerable RemoveTransfers(IAsyncEnumerable transfers, CancellationToken ct) + { + var successes = new List(); + var errors = new List(); + + var operationsToUpdate = new List(); + + await foreach (var transfer in transfers.WithCancellation(ct)) + { + var result = await transfersRepository.Remove(transfer, ct); + if (result.IsSuccess) + { + successes.Add(new TransferRemoved(transfer)); + foreach (var operation in transfer.Cast()) + { + operation.Untag(TransferTags.Transfer); + operation.Untag(TransferTags.Source); + operation.Untag(TransferTags.Sink); + + operationsToUpdate.Add(operation); + } + } + else + { + errors.AddRange(result.Errors); + } + } + + if (operationsToUpdate.Any()) + { + var budgets = await Manager.GetOwnedBudgets(ct); + foreach (var operations in operationsToUpdate.GroupBy(o => o.Budget.Id)) + { + var budget = budgets.FirstOrDefault(b => b.Id == operations.Key); + if (budget is null) + { + errors.AddRange(operations.Select(o => + new BudgetDoesNotExistError(operations.Key).WithOperationId(o))); + } + else + { + var result = await Update(operationsToUpdate.ToAsyncEnumerable(), budget, new (null, TaggingMode.Skip), ct); + successes.AddRange(result.Successes); + errors.AddRange(result.Errors); + } + } + } + + return new Result().WithSuccesses(successes).WithErrors(errors); + } } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/BudgetManager.cs b/src/Application/NVs.Budget.Application/Services/Accounting/BudgetManager.cs index 12c6a61b..d93a44d6 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/BudgetManager.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/BudgetManager.cs @@ -1,10 +1,10 @@ using FluentResults; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Errors.Accounting; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Application.Services.Accounting.Results.Errors; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; namespace NVs.Budget.Application.Services.Accounting; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Duplicates/DuplicatesDetector.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Duplicates/DuplicatesDetector.cs index 055b5b7d..6d4a913b 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Duplicates/DuplicatesDetector.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Duplicates/DuplicatesDetector.cs @@ -1,5 +1,5 @@ using System.Collections.ObjectModel; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Options; namespace NVs.Budget.Application.Services.Accounting.Duplicates; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Reckon/Reckoner.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Reckon/Reckoner.cs index ee24b246..2549a5b6 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Reckon/Reckoner.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Reckon/Reckoner.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Queries; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Application.Services.Accounting.Duplicates; @@ -114,7 +114,7 @@ private TrackedOperation AsTrackedOperation(Operation operation) { var result = new TrackedOperation( operation.Id, operation.Timestamp, operation.Amount, operation.Description, - AsTrackedAccount(operation.Budget), operation.Tags, operation.Attributes.AsReadOnly() + AsTrackedBudget(operation.Budget), operation.Tags, operation.Attributes.AsReadOnly() ); result.TagEphemeral(); @@ -122,6 +122,6 @@ private TrackedOperation AsTrackedOperation(Operation operation) return result; } - private TrackedBudget AsTrackedAccount(Domain.Entities.Accounts.Budget budget) => budget is TrackedBudget ta ? ta : new TrackedBudget(budget.Id, budget.Name, budget.Owners, [], [], LogbookCriteria.Universal); + private TrackedBudget AsTrackedBudget(Domain.Entities.Budgets.Budget budget) => budget is TrackedBudget ta ? ta : new TrackedBudget(budget.Id, budget.Name, budget.Owners, [], [], LogbookCriteria.Universal); } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/ReckonerBase.cs b/src/Application/NVs.Budget.Application/Services/Accounting/ReckonerBase.cs index 307646b1..e953610a 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/ReckonerBase.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/ReckonerBase.cs @@ -1,5 +1,5 @@ using System.Linq.Expressions; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Services; using NVs.Budget.Utilities.Expressions; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/ImportResultBuilder.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/ImportResultBuilder.cs index 3f0ba63d..871ebddb 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/ImportResultBuilder.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/ImportResultBuilder.cs @@ -1,8 +1,5 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Results; +using NVs.Budget.Application.Contracts.Results; using NVs.Budget.Application.Services.Accounting.Duplicates; -using NVs.Budget.Application.Services.Accounting.Results.Successes; namespace NVs.Budget.Application.Services.Accounting.Results; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/BudgetAdded.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/BudgetAdded.cs index a28cff30..6b051290 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/BudgetAdded.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/BudgetAdded.cs @@ -5,8 +5,8 @@ namespace NVs.Budget.Application.Services.Accounting.Results.Successes; internal class BudgetAdded : Success { - public BudgetAdded(Domain.Entities.Accounts.Budget budget) : base("Budget was successfully added!") + public BudgetAdded(Domain.Entities.Budgets.Budget budget) : base("Budget was successfully added!") { - this.WithOperationId(budget); + this.WithBudgetId(budget); } } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationAdded.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationAdded.cs index d0689ed1..8a70c290 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationAdded.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationAdded.cs @@ -8,6 +8,6 @@ internal class OperationAdded : Success { public OperationAdded(Operation operation) : base("Transaction was successfully added!") { - this.WithTransactionId(operation); + this.WithOperationId(operation); } } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationRemoved.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationRemoved.cs index 60f4f354..313285c8 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationRemoved.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationRemoved.cs @@ -8,6 +8,6 @@ internal class OperationRemoved : Success { public OperationRemoved(Operation operation) : base("Transaction was successfully removed!") { - this.WithTransactionId(operation); + this.WithOperationId(operation); } } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationUpdated.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationUpdated.cs index 9a26ae3c..95e630a7 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationUpdated.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/OperationUpdated.cs @@ -8,6 +8,6 @@ internal class OperationUpdated : Success { public OperationUpdated(Operation operation) : base("Transaction was successfully updated!") { - this.WithTransactionId(operation); + this.WithOperationId(operation); } } diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferAdded.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferAdded.cs index aff846c4..b59cb7ab 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferAdded.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferAdded.cs @@ -1,5 +1,5 @@ using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Services.Accounting.Results.Successes; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferRemoved.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferRemoved.cs new file mode 100644 index 00000000..bb643bbc --- /dev/null +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferRemoved.cs @@ -0,0 +1,13 @@ +using FluentResults; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Application.Services.Accounting.Results.Successes; + +internal class TransferRemoved : Success +{ + public TransferRemoved(TrackedTransfer transfer):base("Transfer successfully removed!") + { + Metadata.Add(nameof(transfer.Source), transfer.Source.Id); + Metadata.Add(nameof(transfer.Sink), transfer.Sink.Id); + } +} \ No newline at end of file diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferTracked.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferTracked.cs index 433286a2..81c97cda 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferTracked.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/Successes/TransferTracked.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Services.Accounting.Results.Successes; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Results/UpdateResultBuilder.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Results/UpdateResultBuilder.cs index dee7886d..195f1c7d 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Results/UpdateResultBuilder.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Results/UpdateResultBuilder.cs @@ -1,7 +1,6 @@ using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Results; -using NVs.Budget.Application.Services.Accounting.Duplicates; using NVs.Budget.Application.Services.Accounting.Results.Successes; namespace NVs.Budget.Application.Services.Accounting.Results; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TaggingFunc.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TaggingFunc.cs index 3bf4a67b..107bb750 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TaggingFunc.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TaggingFunc.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.ValueObjects; namespace NVs.Budget.Application.Services.Accounting.Tags; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TagsManager.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TagsManager.cs index e4157da0..e7265865 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TagsManager.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Tags/TagsManager.cs @@ -1,7 +1,6 @@ using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.ValueObjects; -using NVs.Budget.Utilities.Expressions; namespace NVs.Budget.Application.Services.Accounting.Tags; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransferDetector.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransferDetector.cs index 149e25f6..ddca2e54 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransferDetector.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransferDetector.cs @@ -1,10 +1,8 @@ -using System.Linq.Expressions; -using FluentResults; +using FluentResults; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Services.Accounting.Results.Errors; using NVs.Budget.Domain.Extensions; -using NVs.Budget.Utilities.Expressions; namespace NVs.Budget.Application.Services.Accounting.Transfers; diff --git a/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransfersListBuilder.cs b/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransfersListBuilder.cs index af59d40e..0aed4005 100644 --- a/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransfersListBuilder.cs +++ b/src/Application/NVs.Budget.Application/Services/Accounting/Transfers/TransfersListBuilder.cs @@ -1,8 +1,8 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Application.Services.Accounting.Transfers; -internal class TransfersListBuilder(TransferDetector detector) +internal class TransfersListBuilder(TransferDetector detector, DetectionAccuracy? tagIf) { private readonly List _parts = new(); @@ -20,12 +20,14 @@ internal class TransfersListBuilder(TransferDetector detector) { _parts.Remove(part); - source.Tag(TransferTags.Transfer); - source.Tag(TransferTags.Source); - - sink.Tag(TransferTags.Transfer); - sink.Tag(TransferTags.Sink); + if (tagIf.HasValue && detectionResult.Value.Accuracy >= tagIf.Value) + { + source.Tag(TransferTags.Transfer); + source.Tag(TransferTags.Source); + sink.Tag(TransferTags.Transfer); + sink.Tag(TransferTags.Sink); + } return detectionResult.Value; } diff --git a/src/Application/NVs.Budget.Infrastructure.Identity.Contracts/IIdentityService.cs b/src/Application/NVs.Budget.Infrastructure.Identity.Contracts/IIdentityService.cs index b7fb7367..b38562f0 100644 --- a/src/Application/NVs.Budget.Infrastructure.Identity.Contracts/IIdentityService.cs +++ b/src/Application/NVs.Budget.Infrastructure.Identity.Contracts/IIdentityService.cs @@ -4,5 +4,5 @@ namespace NVs.Budget.Infrastructure.Identity.Contracts; public interface IIdentityService { - public Task GetCurrentUser(CancellationToken ct); + Task GetCurrentUser(CancellationToken ct); } diff --git a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IBudgetsRepository.cs b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IBudgetsRepository.cs index f53aa43f..f37bae69 100644 --- a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IBudgetsRepository.cs +++ b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IBudgetsRepository.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; namespace NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IExchangeRatesRepository.cs b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IExchangeRatesRepository.cs index a190d1ac..08fa2526 100644 --- a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IExchangeRatesRepository.cs +++ b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IExchangeRatesRepository.cs @@ -1,5 +1,5 @@ using NMoneys; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.ValueObjects; namespace NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOperationsRepository.cs b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOperationsRepository.cs index f8b3ede1..f8fa4432 100644 --- a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOperationsRepository.cs +++ b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOperationsRepository.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOwnersRepository.cs b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOwnersRepository.cs index c5262413..ea379ccd 100644 --- a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOwnersRepository.cs +++ b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/IOwnersRepository.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using FluentResults; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/ITransfersRepository.cs b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/ITransfersRepository.cs index 4d59ad7b..d9cde832 100644 --- a/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/ITransfersRepository.cs +++ b/src/Application/NVs.Budget.Infrastructure.Persistence.Contracts/Accounting/ITransfersRepository.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/AbstractVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/AbstractVerb.cs deleted file mode 100644 index fbcd9ceb..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/AbstractVerb.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CommandLine; -using MediatR; - -namespace NVs.Budget.Controllers.Console.Contracts.Commands; - -public class AbstractVerb : IRequest -{ - [Option('o', "output", Required = false, HelpText = "Output path. If no value is set, app will use value from configuration")] - public string? OutputPath { get; set; } - - [Option('e', "errors", Required = false, HelpText = "Errors path. If no value is set, app will use value from configuration")] - public string? ErrorsPath { get; set; } - -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCode.cs b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCode.cs deleted file mode 100644 index eea094c4..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCode.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NVs.Budget.Controllers.Console.Contracts.Commands; - -[Flags] -public enum ExitCode -{ - Success = 0, - OperationError = 3, - ArgumentsError = 5, - Cancelled = 7, - UnexpectedResult = 127 -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCodeHelpers.cs b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCodeHelpers.cs deleted file mode 100644 index 4cac0a0e..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/ExitCodeHelpers.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Controllers.Console.Contracts.Commands; - -public static class ExitCodeHelpers -{ - public static ExitCode ToExitCode(this IResultBase result) - { - return result.IsSuccess ? ExitCode.Success : ExitCode.OperationError; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/SuperVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/SuperVerb.cs deleted file mode 100644 index c8775954..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Commands/SuperVerb.cs +++ /dev/null @@ -1,10 +0,0 @@ -using CommandLine; - -namespace NVs.Budget.Controllers.Console.Contracts.Commands; - -public abstract class SuperVerb(Type[] verbs) : AbstractVerb -{ - public Type[] Verbs { get; } = verbs; - - [Value(0)] public IEnumerable? Args { get; set; } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Errors/ExceptionBasedError.cs b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Errors/ExceptionBasedError.cs deleted file mode 100644 index bd3ca463..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/Errors/ExceptionBasedError.cs +++ /dev/null @@ -1,32 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Controllers.Console.Contracts.Errors; - -public class ExceptionBasedError : IError -{ - public ExceptionBasedError(Exception e, string? message = null) - { - Message = message ?? e.Message; - Reasons = new List(); - Metadata = new Dictionary(); - - foreach (var key in e.Data.Keys) - { - var stringKey = GetKey(key); - Metadata[stringKey] = e.Data[stringKey] ?? "(null)"; - } - - if (e.InnerException != null) - { - Reasons.Add(new ExceptionBasedError(e.InnerException)); - } - } - - private int keyCounter; - - private string GetKey(object? key) => key as string ?? $"{keyCounter++} {key}"; - - public string Message { get; } - public Dictionary Metadata { get; } - public List Reasons { get; } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/NVs.Budget.Controllers.Console.Contracts.csproj b/src/Controllers/NVs.Budget.Controllers.Console.Contracts/NVs.Budget.Controllers.Console.Contracts.csproj deleted file mode 100644 index 49581fc7..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Contracts/NVs.Budget.Controllers.Console.Contracts.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/CronBasedNamedRangeSeriesBuilderShould.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/CronBasedNamedRangeSeriesBuilderShould.cs deleted file mode 100644 index 012592ac..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/CronBasedNamedRangeSeriesBuilderShould.cs +++ /dev/null @@ -1,35 +0,0 @@ -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using NVs.Budget.Controllers.Console.Handlers.Utils; - -namespace NVs.Budget.Controllers.Console.Handlers.Tests; - -public class CronBasedNamedRangeSeriesBuilderShould -{ - [Fact] - public void GenerateValidSchedule() - { - var from = new DateTime(DateTime.Now.Year, 1, 1); - var till = new DateTime(DateTime.Now.Year + 1, 1, 1); - var expression = "0 0 1 * *"; - - var ranges = new CronBasedNamedRangeSeriesBuilder().GetRanges(from, till, expression); - ranges.Should().BeSuccess(); - var values = ranges.Value.ToList(); - values.Count.Should().Be(12); - var i = 1; - while (i < values.Count) - { - var prev = values[i - 1]; - var curr = values[i]; - - prev.From.Day.Should().Be(1); - prev.From.Month.Should().Be(i); - prev.Till.Should().Be(curr.From); - i++; - } - - var last = values.Last(); - last.Till.Should().Be(till); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Behaviors/ResultWritingPostProcessor.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Behaviors/ResultWritingPostProcessor.cs deleted file mode 100644 index d1f2575b..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Behaviors/ResultWritingPostProcessor.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FluentResults; -using MediatR; -using MediatR.Pipeline; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Behaviors; - -internal class ResultWritingPostProcessor(IResultWriter writer) : IRequestPostProcessor -where TRequest : IRequest -where TResponse : IResultBase -{ - public Task Process(TRequest request, TResponse response, CancellationToken cancellationToken) => writer.Write(response, cancellationToken); -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/AddVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/AddVerb.cs deleted file mode 100644 index 83a633f5..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/AddVerb.cs +++ /dev/null @@ -1,28 +0,0 @@ -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("add", HelpText = "Adds new budget")] -internal class AddVerb : AbstractVerb -{ - [Value(0, MetaName = "Budget name", Required = true, HelpText = "Name of a budget")] - public IEnumerable Name { get; set; } = Enumerable.Empty(); -} - -internal class AddVerbHandler(IBudgetManager manager, IResultWriter> writer) : IRequestHandler -{ - public async Task Handle(AddVerb request, CancellationToken cancellationToken) - { - var name = string.Join(' ', request.Name); - var result = await manager.Register(new(name), cancellationToken); - await writer.Write(result, cancellationToken); - - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetDetailsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetDetailsVerb.cs deleted file mode 100644 index 76a3ac41..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetDetailsVerb.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CommandLine; -using JetBrains.Annotations; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -internal class BudgetDetailsVerb : AbstractVerb -{ - private string _budgetId = string.Empty; - - [Value(0, Required = true, MetaName = "Budget id")] - public string Id - { - get => _budgetId; - [UsedImplicitly] set - { - _budgetId = value; - CsvReadingOptionsPath ??= $"{value}_csv-reading-options.yml"; - LogbookCriteriaPath ??= $"{value}_logbook-criteria.yml"; - TaggingCriteriaPath ??= $"{value}_tagging-criteria.yml"; - TransferCriteriaPath ??= $"{value}_transfer-criteria.yml"; - } - } - - [Option("csv-reading-options", HelpText = "Path to YAML file with CSV reading options. If not defined, app will use budget-related name")] - public string? CsvReadingOptionsPath { get; [UsedImplicitly] set; } - - [Option("tagging-criteria", HelpText = "Path to YAML file with tagging criteria. If not defined, app will use budget-related name")] - public string? TaggingCriteriaPath { get; [UsedImplicitly] set; } - - [Option("transfer-criteria", HelpText = "Path to YAML file with transfer criteria. If not defined, app will use budget-related name")] - public string? TransferCriteriaPath { get; [UsedImplicitly] set; } - - [Option("logbook-criteria", HelpText = "Path to YAML file with logbook criteria. If defined, transfer criteria will be updated")] - public string? LogbookCriteriaPath { get; [UsedImplicitly] set; } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetsVerb.cs deleted file mode 100644 index aef4d0f3..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/BudgetsVerb.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("budget", HelpText = "Budgets handling")] -internal class BudgetsVerb() : SuperVerb([typeof(ListVerb), typeof(DetailsVerb), typeof(AddVerb), typeof(MergeVerb), typeof(UpdateVerb)]); diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/DetailsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/DetailsVerb.cs deleted file mode 100644 index e4591ec8..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/DetailsVerb.cs +++ /dev/null @@ -1,62 +0,0 @@ -using CommandLine; -using FluentResults; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("details", HelpText = "Display details of specific budget")] -internal sealed class DetailsVerb : BudgetDetailsVerb; - -internal class DetailsVerbHandler( - IBudgetManager manager, - IBudgetSpecificSettingsRepository csvSettingsRepo, - IObjectWriter budgetWriter, - IObjectWriter optionsWriter, - IObjectWriter logbookWriter, - IObjectWriter transferWriter, - IObjectWriter taggingWriter, - IResultWriter writer, - IOutputStreamProvider streamProvider, - IOptionsSnapshot outputOptions) : IRequestHandler -{ - public async Task Handle(DetailsVerb request, CancellationToken cancellationToken) - { - if (!Guid.TryParse(request.Id, out var id)) - { - await writer.Write(Result.Fail("Input value is not a guid"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var budgets = await manager.GetOwnedBudgets(cancellationToken); - var budget = budgets.FirstOrDefault(b => b.Id == id); - if (budget is null) - { - await writer.Write(Result.Fail("Budget with given id does not exists"), cancellationToken); - return ExitCode.ArgumentsError; - } - - await budgetWriter.Write(budget, cancellationToken); - var output = await streamProvider.GetOutput(outputOptions.Value.OutputStreamName); - await output.WriteLineAsync($"Logbook: {request.LogbookCriteriaPath}"); - await output.WriteLineAsync($"Transfers: {request.TransferCriteriaPath}"); - await output.WriteLineAsync($"Tags: {request.TaggingCriteriaPath}"); - await output.WriteLineAsync($"CSV: {request.CsvReadingOptionsPath}"); - - - await logbookWriter.Write(budget.LogbookCriteria, request.LogbookCriteriaPath!, cancellationToken); - await transferWriter.Write(budget.TransferCriteria, request.TransferCriteriaPath!, cancellationToken); - await taggingWriter.Write(budget.TaggingCriteria, request.TaggingCriteriaPath!, cancellationToken); - - var config = await csvSettingsRepo.GetReadingOptionsFor(budget, cancellationToken); - await optionsWriter.Write(config, request.CsvReadingOptionsPath!, cancellationToken); - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/ListVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/ListVerb.cs deleted file mode 100644 index 8d9387b0..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/ListVerb.cs +++ /dev/null @@ -1,22 +0,0 @@ -using CommandLine; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.UseCases.Accounts; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("list", true, HelpText = "Lists owned budgets for current user")] -internal class ListVerb : AbstractVerb; - -internal class ListVerbHandler(IMediator mediator, IObjectWriter writer) : IRequestHandler -{ - public async Task Handle(ListVerb request, CancellationToken cancellationToken) - { - var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), cancellationToken); - await writer.Write(budgets, cancellationToken); - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/MergeVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/MergeVerb.cs deleted file mode 100644 index 2b912b43..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/MergeVerb.cs +++ /dev/null @@ -1,42 +0,0 @@ -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.UseCases.Accounts; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("merge", HelpText = "Merges budgets into the last one")] -internal class MergeVerb : AbstractVerb -{ - [Value(0, MetaName = "Budget ids to merge (min 2). All operations will be moved from budgets into the last one provided")] - public IEnumerable BudgetIds { get; set; } = Enumerable.Empty(); - - [Option("purge", Default = false, HelpText = "Purge empty accounts")] - public bool Purge { get; set; } -} - -internal class MergeVerbHandler(IMediator mediator, IResultWriter resultWriter) : IRequestHandler -{ - public async Task Handle(MergeVerb request, CancellationToken cancellationToken) - { - var ids = new List(); - foreach (var budgetId in request.BudgetIds) - { - if (!Guid.TryParse(budgetId, out var i)) - { - var parseResult = Result.Fail(new Error("Failed to parse Guid").WithMetadata("Value", budgetId)); - await resultWriter.Write(parseResult, cancellationToken); - return ExitCode.ArgumentsError; - } - - ids.Add(i); - } - - var rq = new MergeAccountsRequest(ids, request.Purge); - var result = await mediator.Send(rq, cancellationToken); - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/UpdateVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/UpdateVerb.cs deleted file mode 100644 index bc848b34..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Budgets/UpdateVerb.cs +++ /dev/null @@ -1,261 +0,0 @@ -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Budgets; - -[Verb("update", HelpText = "Updates budget-specific settings")] -internal class UpdateVerb : BudgetDetailsVerb -{ - [Option('n', "name", HelpText = "New name. If set, budget will be renamed")] - public string? Name { get; [UsedImplicitly] set; } -} - -internal class UpdateVerbHandler( - IInputStreamProvider input, - ICsvReadingOptionsReader reader, - ITransferCriteriaReader transferCriteriaReader, - ILogbookCriteriaReader logbookCriteriaReader, - ITaggingCriteriaReader taggingCriteriaReader, - IBudgetManager manager, - IBudgetSpecificSettingsRepository repository, - IResultWriter resultWriter - ) : IRequestHandler -{ - public async Task Handle(UpdateVerb request, CancellationToken cancellationToken) - { - if (!Guid.TryParse(request.Id, out var id)) - { - await resultWriter.Write(Result.Fail("Input value is not a guid"), cancellationToken); - return ExitCode.ArgumentsError; - } - - if (string.IsNullOrEmpty(request.Name) - && string.IsNullOrEmpty(request.CsvReadingOptionsPath) - && string.IsNullOrEmpty(request.TaggingCriteriaPath) - && string.IsNullOrEmpty(request.TransferCriteriaPath) - && string.IsNullOrEmpty(request.LogbookCriteriaPath)) - { - await resultWriter.Write(Result.Fail("No options to update given"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var budgets = await manager.GetOwnedBudgets(cancellationToken); - var budget = budgets.FirstOrDefault(b => b.Id == id); - if (budget is null) - { - await resultWriter.Write(Result.Fail("Budget with given id does not exist"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var hasChanges = TryRename(request, budget); - - var changeTaggingCriteriaResult = await TryChangeTaggingCriteria(request, budget, cancellationToken); - if (changeTaggingCriteriaResult.IsFailed) - { - return ExitCode.ArgumentsError; - } - - hasChanges = hasChanges || changeTaggingCriteriaResult.Value; - - var changeTransferCriteriaResult = await TryChangeTransferCriteria(request, budget, cancellationToken); - if (changeTransferCriteriaResult.IsFailed) - { - return ExitCode.ArgumentsError; - } - - hasChanges = hasChanges || changeTransferCriteriaResult.Value; - - var changeLogbookCriteriaResult = await TryChangeLogbookCriteria(request, budget, cancellationToken); - if (changeLogbookCriteriaResult.IsFailed) - { - return ExitCode.ArgumentsError; - } - - hasChanges = hasChanges || changeLogbookCriteriaResult.Value; - - if (hasChanges) - { - var result = await manager.Update(budget, cancellationToken); - if (!result.IsSuccess) - { - await resultWriter.Write(result, cancellationToken); - return ExitCode.OperationError; - } - } - - - if (!string.IsNullOrEmpty(request.CsvReadingOptionsPath)) - { - if (!File.Exists(request.CsvReadingOptionsPath)) - { - await resultWriter.Write(Result.Fail("CSV reading options file does not exist"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var stream = await input.GetInput(request.CsvReadingOptionsPath); - if (stream.IsFailed) - { - await resultWriter.Write(stream.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - var config = await reader.ReadFrom(stream.Value, cancellationToken); - if (config.IsFailed) - { - await resultWriter.Write(config.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - var result = await repository.UpdateReadingOptionsFor(budget, config.Value, cancellationToken); - if (result.IsFailed) - { - await resultWriter.Write(result, cancellationToken); - return ExitCode.OperationError; - } - } - - return ExitCode.Success; - } - - private async Task> TryChangeLogbookCriteria(UpdateVerb request, TrackedBudget budget, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(request.LogbookCriteriaPath)) - { - return false; - } - - if (!File.Exists(request.LogbookCriteriaPath)) - { - var result = Result.Fail("Logbook criteria file does not exist"); - await resultWriter.Write(result, cancellationToken); - return result; - } - - var stream = await input.GetInput(request.LogbookCriteriaPath); - if (!stream.IsSuccess) - { - var result = stream.ToResult(); - await resultWriter.Write(result, cancellationToken); - return result; - } - - var criteria = await logbookCriteriaReader.ReadFrom(stream.Value, cancellationToken); - if (criteria.IsFailed) - { - var result = criteria.ToResult(); - await resultWriter.Write(result, cancellationToken); - return result; - } - - budget.SetLogbookCriteria(criteria.Value); - return true; - } - - private async Task> TryChangeTransferCriteria(UpdateVerb request, TrackedBudget budget, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(request.TransferCriteriaPath)) - { - return false; - } - - if (!File.Exists(request.TransferCriteriaPath)) - { - var result = Result.Fail("Transfer criteria file does not exist"); - await resultWriter.Write(result, cancellationToken); - return result; - } - - var stream = await input.GetInput(request.TransferCriteriaPath); - if (!stream.IsSuccess) - { - var result = stream.ToResult(); - await resultWriter.Write(result, cancellationToken); - return result; - } - - var criteria = await transferCriteriaReader.ReadFrom(stream.Value, cancellationToken).ToListAsync(cancellationToken); - var errors = criteria.Where(r => r.IsFailed).SelectMany(r => r.Errors); - var values = criteria.Where(r => r.IsSuccess).Select(r => r.Value).ToList(); - - Result hasChanges; - if (values.Any()) - { - budget.SetTransferCriteria(values); - hasChanges = Result.Ok(true).WithErrors(errors); - } - else - { - hasChanges = Result.Fail(errors); - } - - await resultWriter.Write(hasChanges.ToResult(), cancellationToken); - return hasChanges; - } - - private async Task> TryChangeTaggingCriteria(UpdateVerb request, TrackedBudget budget, CancellationToken cancellationToken) - { - List? taggingCriteria = null; - if (!string.IsNullOrEmpty(request.TaggingCriteriaPath)) - { - if (!File.Exists(request.TaggingCriteriaPath)) - { - var result = Result.Fail("Tagging criteria file does not exist"); - await resultWriter.Write(result, cancellationToken); - return result; - } - - var stream = await input.GetInput(request.TaggingCriteriaPath); - if (!stream.IsSuccess) - { - var result = stream.ToResult(); - await resultWriter.Write(result, cancellationToken); - return result; - } - - taggingCriteria = new List(); - await foreach (var rule in taggingCriteriaReader.ReadFrom(stream.Value, cancellationToken)) - { - if (rule.IsSuccess) - { - taggingCriteria.Add(rule.Value); - } - else - { - await resultWriter.Write(rule.ToResult(), cancellationToken); - } - } - - if (!taggingCriteria.Any()) - { - taggingCriteria = null; - } - } - - if (taggingCriteria is not null) - { - budget.SetTaggingCriteria(taggingCriteria); - } - - return taggingCriteria is not null; - } - - private static bool TryRename(UpdateVerb request, TrackedBudget budget) - { - if (!string.IsNullOrEmpty(request.Name)) - { - budget.Rename(request.Name); - return true; - } - - return false; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/CriteriaBasedVerbHandler.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/CriteriaBasedVerbHandler.cs deleted file mode 100644 index 8faecb58..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/CriteriaBasedVerbHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands; - -internal class CriteriaBasedVerb : AbstractVerb -{ - [Value(0, MetaName = "Criteria")] - public IEnumerable? Criteria { get; set; } -} - -internal abstract class CriteriaBasedVerbHandler(ICriteriaParser parser, IResultWriter writer, string paramName = "o") : IRequestHandler where T: CriteriaBasedVerb -{ - protected readonly IResultWriter Writer = writer; - - public async Task Handle(T request, CancellationToken cancellationToken) - { - var criteria = string.Join(' ', request.Criteria ?? Enumerable.Empty()); - var criteriaResult = parser.TryParsePredicate(criteria, paramName); - if (!criteriaResult.IsSuccess) - { - await Writer.Write(criteriaResult.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - return await HandleInternal(request, criteriaResult.Value, cancellationToken); - } - - protected abstract Task HandleInternal(T request, Expression> criteriaResultValue, CancellationToken cancellationToken); -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/EditVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/EditVerb.cs deleted file mode 100644 index 7e657a22..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/EditVerb.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Contracts.Options; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("update", HelpText = "Updates tracked operations from the given input")] -internal class UpdateVerb : CriteriaBasedVerb -{ - [Option('b', "budget-id", Required = false, HelpText = "ID of a budget. Optional if user has only one budget, otherwise required")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; - - [Option('f', "file", HelpText = "Path to file with update content. If value is not specified, app will use standard input")] - public string? FilePath { get; [UsedImplicitly] set; } - - [Option('c', "confidence", HelpText = "Register found transfers that matches confidence level")] - public string? ConfidenceLevel { get; [UsedImplicitly] set; } -} - -internal class UpdateVerbHandler(IMediator mediator, IBudgetManager manager, IInputStreamProvider streams, IOperationsReader reader, ICriteriaParser parser, IResultWriter writer) : CriteriaBasedVerbHandler(parser, writer) -{ - protected override async Task HandleInternal(UpdateVerb request, Expression> criteriaResultValue, CancellationToken ct) - { - TrackedBudget? budget; - var budgets = await manager.GetOwnedBudgets(ct); - if (budgets.Count == 1) - { - budget = budgets.Single(); - } - else - { - if (!Guid.TryParse(request.BudgetId, out var id)) - { - await Writer.Write(Result.Fail(new Error("Given budget id is not a guid").WithMetadata("Value", request.BudgetId)), ct); - return ExitCode.ArgumentsError; - } - - budget = budgets.FirstOrDefault(b => b.Id == id); - - if (budget is null) - { - await Writer.Write(Result.Fail(new BudgetDoesNotExistError(id)), ct); - return ExitCode.ArgumentsError; - } - } - - var steamReader = await streams.GetInput(request.FilePath ?? string.Empty); - if (!steamReader.IsSuccess) - { - await Writer.Write(steamReader.ToResult(), ct); - return ExitCode.ArgumentsError; - } - - DetectionAccuracy? accuracy = null; - if (request.ConfidenceLevel is not null) - { - if (Enum.TryParse(request.ConfidenceLevel, out DetectionAccuracy level)) - { - accuracy = level; - } - else - { - await Writer.Write(Result.Fail(new Error("Given apply is not a DetectionAccuracy!").WithMetadata("Value", request.ConfidenceLevel)), ct); - return ExitCode.ArgumentsError; - } - } - - - var exitCodes = new HashSet(); - var operations = reader.ReadTrackedOperation(steamReader.Value, ct).SelectAwait(async r => - { - if (r.IsSuccess) return r.Value; - await Writer.Write(r.ToResult(), ct); - exitCodes.Add(r.ToExitCode()); - return null; - }).Where(o => o is not null); - - var result = await mediator.Send(new UpdateOperationsCommand(operations!, budget, new(accuracy, TaggingMode.Skip)), ct); - exitCodes.Add(result.ToExitCode()); - - return exitCodes.Aggregate((r, e) => r | e); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ImportVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ImportVerb.cs deleted file mode 100644 index 8bb441bb..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ImportVerb.cs +++ /dev/null @@ -1,113 +0,0 @@ -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Contracts.Options; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("import", HelpText = "Import operations from files")] -internal class ImportVerb : AbstractVerb -{ - [Option('b', "budget-id", Required = false, HelpText = "ID of a budget. Optional if user has only one budget, otherwise required")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; - - [Option('d', "dir", Required = true, HelpText = "Directory with files to import")] - public string? DirectoryPath { get; [UsedImplicitly] set; } - - [Option("confidence", Required = false, Default = DetectionAccuracy.Exact, HelpText = "Transfers detection accuracy (Exact or Likely)")] - public DetectionAccuracy DetectionAccuracy { get; [UsedImplicitly] set; } -} - -internal class ImportVerbHandler( - IInputStreamProvider input, - IResultWriter resultWriter, - IBudgetSpecificSettingsRepository settingsRepo, - IBudgetManager manager, - IOperationsReader reader, - IMediator mediator -) : IRequestHandler -{ - public async Task Handle(ImportVerb request, CancellationToken cancellationToken) - { - if (!Directory.Exists(request.DirectoryPath)) - { - return ExitCode.ArgumentsError; - } - - var exitCodes = new HashSet { ExitCode.Success }; - - TrackedBudget? budget; - var budgets = await manager.GetOwnedBudgets(cancellationToken); - if (string.IsNullOrEmpty(request.BudgetId) && budgets.Count == 1) - { - budget = budgets.Single(); - } - else - { - if (!Guid.TryParse(request.BudgetId, out var id)) - { - await resultWriter.Write(Result.Fail(new Error("Given budget id is not a guid").WithMetadata("Value", request.BudgetId)), cancellationToken); - return ExitCode.ArgumentsError; - - } - - budget = budgets.FirstOrDefault(b => b.Id == id); - - if (budget is null) - { - var fail = Result.Fail(new BudgetDoesNotExistError(id)); - await resultWriter.Write(fail, cancellationToken); - return fail.ToExitCode(); - } - } - - var csvOptions = await settingsRepo.GetReadingOptionsFor(budget, cancellationToken); - - var options = new ImportOptions(request.DetectionAccuracy); - - foreach (var file in Directory.EnumerateFiles(request.DirectoryPath)) - { - var fileOptionsResult = csvOptions.GetFileOptionsFor(file); - if (fileOptionsResult.IsFailed) - { - await resultWriter.Write(fileOptionsResult.ToResult(), cancellationToken); - exitCodes.Add(ExitCode.OperationError); - continue; - } - - var fileStreamResult = await input.GetInput(file); - if (fileStreamResult.IsFailed) - { - await resultWriter.Write(fileStreamResult.ToResult(), cancellationToken); - exitCodes.Add(ExitCode.ArgumentsError); - } - else - { - var operations = reader.ReadUnregisteredOperations(fileStreamResult.Value, fileOptionsResult.Value, cancellationToken); - var parsedOperations = operations.SelectAwait(async r => - { - if (r.IsSuccess) return r.Value; - await resultWriter.Write(r.ToResult(), cancellationToken); - exitCodes.Add(r.ToExitCode()); - return null; - - }).Where(o => o is not null); - - var result = await mediator.Send(new ImportOperationsCommand(parsedOperations!,budget, options), cancellationToken); - exitCodes.Add(result.ToExitCode()); - } - } - - return exitCodes.Aggregate((r, e) => r | e); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListDuplicatesVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListDuplicatesVerb.cs deleted file mode 100644 index 4160e24f..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListDuplicatesVerb.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("list-duplicates", isDefault: false, HelpText = "List duplicated operations that matches criteria")] -internal class ListDuplicatesVerb : CriteriaBasedVerb; - -internal class ListDuplicatesVerbHandler(IMediator mediator, - IObjectWriter objectWriter, - ICriteriaParser criteriaParser, - IResultWriter resultWriter -) : CriteriaBasedVerbHandler(criteriaParser, resultWriter) -{ - protected override async Task HandleInternal(ListDuplicatesVerb request, Expression> criteria, CancellationToken cancellationToken) - { - var result = await mediator.Send(new ListDuplicatedOperationsQuery(criteria), cancellationToken); - await objectWriter.Write(result.SelectMany(r => r), cancellationToken); - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListVerb.cs deleted file mode 100644 index 90534a00..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/ListVerb.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using MediatR; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Queries; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("list", isDefault:true, HelpText = "List operations that match given criteria")] -internal class ListVerb : CriteriaBasedVerb -{ - [Option("currency", HelpText = "Output currency code. If none set, original currencies will be used")] - public CurrencyIsoCode? CurrencyIsoCode { get; set; } - - [Option("exclude-transfers", HelpText = "Exclude transfers from the list")] - public bool ExcludeTransfers { get; set; } -} - -internal class ListVerbHandler( - IMediator mediator, - IObjectWriter objectWriter, - ICriteriaParser criteriaParser, - IResultWriter resultWriter -) : CriteriaBasedVerbHandler(criteriaParser, resultWriter) -{ - protected override async Task HandleInternal(ListVerb request, Expression> criteriaResultValue, CancellationToken cancellationToken) - { - var query = new OperationQuery( - criteriaResultValue, - request.CurrencyIsoCode is not null ? Currency.Get(request.CurrencyIsoCode.Value) : null, - request.ExcludeTransfers - ); - - var operations = mediator.CreateStream(new ListOperationsQuery(query), cancellationToken); - - await objectWriter.Write(await operations.ToListAsync(cancellationToken), cancellationToken); - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsStatisticsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsStatisticsVerb.cs deleted file mode 100644 index 2f60fbca..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsStatisticsVerb.cs +++ /dev/null @@ -1,65 +0,0 @@ -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Handlers.Utils; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("stats", HelpText = "Produces aggregated statistic using predefined set of aggregation rules")] -internal class OperationsStatisticsVerb : StatisticsVerb -{ - [Option('b', "budget-id", Required = false, HelpText = "ID of a budget. Optional if user has only one budget, otherwise required")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; -} - -internal class OperationsStatisticsVerbHandler( - IMediator mediator, - IBudgetManager manager, - ILogbookWriter logbookWriter, - IResultWriter writer, - IOutputStreamProvider outputs, - IOptionsSnapshot options, - CronBasedNamedRangeSeriesBuilder seriesBuilder -) : StatisticsVerbHandlerBase(logbookWriter, writer, seriesBuilder, outputs, options.Value) -{ - protected override async Task> GetLogbook(OperationsStatisticsVerb request, CancellationToken ct) - { - TrackedBudget? budget; - var budgets = await manager.GetOwnedBudgets(ct); - if (budgets.Count == 1) - { - budget = budgets.Single(); - } - else - { - if (!Guid.TryParse(request.BudgetId, out var id)) - { - return Result.Fail(new Error("Given budget id is not a guid").WithMetadata("Value", request.BudgetId)); - } - - budget = budgets.FirstOrDefault(b => b.Id == id); - - if (budget is null) - { - return Result.Fail(new BudgetDoesNotExistError(id)); - } - } - - var query = new CalcOperationsStatisticsQuery(budget.LogbookCriteria.GetCriterion(), o => - o.Timestamp >= request.From.ToUniversalTime() - && o.Timestamp < request.Till.ToUniversalTime() - && o.Budget.Id == budget.Id); - - return await mediator.Send(query, ct); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsVerb.cs deleted file mode 100644 index d054b8e3..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/OperationsVerb.cs +++ /dev/null @@ -1,15 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("ops", false, ["o"], HelpText = "Operations handling")] -internal class OperationsVerb() : SuperVerb([ - typeof(ImportVerb), - typeof(ListVerb), - typeof(ListDuplicatesVerb), - typeof(OperationsStatisticsVerb), - typeof(UpdateVerb), - typeof(RetagVerb), - typeof(RemoveVerb) -]); diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RemoveVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RemoveVerb.cs deleted file mode 100644 index 08ea7cac..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RemoveVerb.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("remove", HelpText = "Removes operations that matches criteria")] -internal class RemoveVerb : CriteriaBasedVerb; - -internal class RemoveVerbHandler(IMediator mediator, ICriteriaParser parser, IResultWriter writer) : CriteriaBasedVerbHandler(parser, writer) -{ - protected override async Task HandleInternal(RemoveVerb request, Expression> criteriaResultValue, CancellationToken cancellationToken) - { - var result = await mediator.Send(new RemoveOperationsCommand(criteriaResultValue), cancellationToken); - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RetagVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RetagVerb.cs deleted file mode 100644 index cf529185..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Operations/RetagVerb.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Operations; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Operations; - -[Verb("retag", HelpText = "Retags operations using tagging criteria from config")] -internal class RetagVerb : CriteriaBasedVerb -{ - [Option('b', "budget-id", Required = false, HelpText = "ID of a budget. Optional if user has only one budget, otherwise required")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; - - [Option("from-scratch", HelpText = "Clear old tags")] - public bool FromScratch { get; [UsedImplicitly] set; } -} - -internal class RetagVerbHandler(IMediator mediator, ICriteriaParser parser, IResultWriter writer, IBudgetManager manager) : CriteriaBasedVerbHandler(parser, writer) -{ - protected override async Task HandleInternal(RetagVerb request, Expression> criteriaResultValue, CancellationToken ct) - { - TrackedBudget? budget; - var budgets = await manager.GetOwnedBudgets(ct); - if (string.IsNullOrEmpty(request.BudgetId) && budgets.Count == 1) - { - budget = budgets.Single(); - } - else - { - if (!Guid.TryParse(request.BudgetId, out var id)) - { - await Writer.Write(Result.Fail(new Error("Given budget id is not a guid").WithMetadata("Value", request.BudgetId)), ct); - return ExitCode.ArgumentsError; - - } - - budget = budgets.FirstOrDefault(b => b.Id == id); - - if (budget is null) - { - var fail = Result.Fail(new BudgetDoesNotExistError(id)); - await Writer.Write(fail, ct); - return fail.ToExitCode(); - } - } - - var command = new RetagOperationsCommand(criteriaResultValue, budget, request.FromScratch); - var result = await mediator.Send(command, ct); - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/ListOwnersVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/ListOwnersVerb.cs deleted file mode 100644 index 2562e890..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/ListOwnersVerb.cs +++ /dev/null @@ -1,43 +0,0 @@ -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.UseCases.Owners; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Owners; - -[Verb("list", isDefault: true, HelpText = "Produces list of owners, tracked by system")] -internal class ListOwnersVerb : AbstractVerb -{ - [Option('p', "param-name", HelpText = "Criteria parameter name", Default = "o")] - public string ParamName { get; [UsedImplicitly] set; } = "o"; - - [Value(0)] public IEnumerable? Criteria { get; [UsedImplicitly] set; } -} - -[UsedImplicitly] -internal class ListOwnersVerbHandler(IMediator mediator, ICriteriaParser parser, IResultWriter resultWriter, IObjectWriter writer) : IRequestHandler -{ - public async Task Handle(ListOwnersVerb request, CancellationToken cancellationToken) - { - var expression = request.Criteria?.Aggregate(string.Empty, (a,i) => a + " " + i) ?? string.Empty; - var criteria = parser.TryParsePredicate(expression, request.ParamName); - if (criteria.IsFailed) - { - await resultWriter.Write(criteria.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - var owners = await mediator.Send(new ListOwnersQuery(criteria.Value), cancellationToken); - foreach (var owner in owners) - { - await writer.Write(owner, cancellationToken); - } - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/OwnersVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/OwnersVerb.cs deleted file mode 100644 index 9400bfb4..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/OwnersVerb.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CommandLine; -using JetBrains.Annotations; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Owners; - -[Verb("owners", HelpText = "Owners management"), UsedImplicitly] -internal class OwnersVerb() : SuperVerb([typeof(ListOwnersVerb), typeof(SelfRegisterVerb)]); diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/SelfRegisterVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/SelfRegisterVerb.cs deleted file mode 100644 index 2aba39fc..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Owners/SelfRegisterVerb.cs +++ /dev/null @@ -1,24 +0,0 @@ -using CommandLine; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.UseCases.Owners; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.Identity.Contracts; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Owners; - -[Verb("self-register")] -internal class SelfRegisterVerb : AbstractVerb; - -[UsedImplicitly] -internal class SelfRegisterVerbHandler(IMediator mediator, IIdentityService identityService) : IRequestHandler -{ - public async Task Handle(SelfRegisterVerb request, CancellationToken cancellationToken) - { - var user = await identityService.GetCurrentUser(cancellationToken); - - var result = await mediator.Send(new RegisterOwnerCommand(user), cancellationToken); - - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerb.cs deleted file mode 100644 index cc8b486c..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerb.cs +++ /dev/null @@ -1,26 +0,0 @@ -using CommandLine; -using JetBrains.Annotations; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands; - -internal class StatisticsVerb : AbstractVerb -{ - [Option('p', "logbook-path", Required = false, HelpText = "A path to logbook to write. If not set app will create a new file with time-based name in a working dir")] - public string LogbookPath { get; [UsedImplicitly] set; } = $"stats_{DateTime.Now:yyyy-MM-dd_hhmmss}.xlsx"; - - [Option('f', "from", Required = false, HelpText = "Date from. If not set app will use beginning of the current year")] - public DateTime From { get; [UsedImplicitly] set; } = new(new DateOnly(DateTime.Now.Year, 1, 1), new TimeOnly(0, 0, 0), DateTime.Now.Kind); - - [Option('t', "till", Required = false, HelpText = "Date till. If not set app will use current moment")] - public DateTime Till { get; [UsedImplicitly] set; } = DateTime.Now; - - [Option('s', "schedule", Default = "0 0 1 * *", HelpText = "Cron expression to generate time ranges")] - public string? Schedule { get; [UsedImplicitly] set; } - - [Option("with-counts", HelpText = "Write operations count for each category")] - public bool WithCounts { get; [UsedImplicitly] set; } - - [Option("with-operations", HelpText = "Write list of operations for each category")] - public bool WithOperations { get; [UsedImplicitly] set; } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerbHandlerBase.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerbHandlerBase.cs deleted file mode 100644 index faf9f9cb..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/StatisticsVerbHandlerBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using FluentResults; -using MediatR; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Controllers.Console.Handlers.Utils; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands; - -internal abstract class StatisticsVerbHandlerBase( - ILogbookWriter logbookWriter, - IResultWriter writer, - CronBasedNamedRangeSeriesBuilder seriesBuilder, - IOutputStreamProvider outputs, - OutputOptions options) : IRequestHandler where T : StatisticsVerb -{ - private Result> GetRanges(DateTime from, DateTime till, string? schedule) - { - return string.IsNullOrEmpty(schedule) - ? new NamedRange[]{ new (string.Empty, from, till) } - : seriesBuilder.GetRanges(from, till, schedule); - } - - public async Task Handle(T request, CancellationToken cancellationToken) - { - - - var ranges = GetRanges(request.From, request.Till, request.Schedule); - await writer.Write(ranges.ToResult(), cancellationToken); - if (!ranges.IsSuccess) - { - return ranges.ToExitCode(); - } - - var output = await outputs.GetOutput(options.OutputStreamName); - await output.WriteLineAsync($"Logbook: {request.LogbookPath}"); - - var result = await GetLogbook(request, cancellationToken); - await writer.Write(result.ToResult(), cancellationToken); - - await logbookWriter.Write(result.ValueOrDefault, - new LogbookWritingOptions( - request.LogbookPath, - request.WithCounts, - request.WithOperations, - ranges.Value), - cancellationToken - ); - - return result.ToExitCode(); - } - - protected abstract Task> GetLogbook(T request, CancellationToken ct); -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/SuperVerbHandler.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/SuperVerbHandler.cs deleted file mode 100644 index fdc22182..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/SuperVerbHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CommandLine; -using CommandLine.Text; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands; - -internal class SuperVerbHandler(IMediator mediator, Parser parser, IOutputOptionsChanger outputOptionsChanger, IOutputStreamProvider streams, IOptionsSnapshot options) : IRequestHandler where T : SuperVerb -{ - public async Task Handle(T request, CancellationToken cancellationToken) - { - if (!string.IsNullOrEmpty(request.OutputPath)) - { - outputOptionsChanger.SetOutputStreamName(request.OutputPath); - } - - if (!string.IsNullOrEmpty(request.ErrorsPath)) - { - outputOptionsChanger.SetErrorStreamName(request.ErrorsPath); - } - - var parserResult = parser.ParseArguments(request.Args, request.Verbs); - var result = await parserResult.MapResult(o => mediator.Send(o, cancellationToken), async errs => - { - var helpText = HelpText.AutoBuild(parserResult); - var errors = errs as Error[] ?? errs.ToArray(); - var isHelp = errors.IsHelp() || errors.IsVersion(); - var writer = isHelp - ? await streams.GetOutput(options.Value.OutputStreamName) - : await streams.GetError(options.Value.ErrorStreamName); - - await writer.WriteLineAsync(helpText); - await writer.FlushAsync(cancellationToken); - return (object?)(isHelp ? ExitCode.Success : ExitCode.ArgumentsError); - }); - - if (result is ExitCode value) - { - return value; - } - - throw new InvalidOperationException("Unexpected object given!") - { - Data = { { "result", result } } - }; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestImportVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestImportVerb.cs deleted file mode 100644 index 3ce22c3b..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestImportVerb.cs +++ /dev/null @@ -1,114 +0,0 @@ -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Test; - -[Verb("import", HelpText = "Test CsvReadingOptions for particular file")] -internal class TestImportVerb : AbstractVerb -{ - [Option('f', "file", Required = true, HelpText = "Incoming file to test")] - public string? FilePath { get; [UsedImplicitly] set; } - - [Option('b', "budget", Required = true, HelpText = "ID of a budget to import operations to")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; -} - -internal class TestImportVerbHandler( - IBudgetManager manager, - IBudgetSpecificSettingsRepository settingsRepository, - IInputStreamProvider input, - IOperationsReader reader, - IOutputStreamProvider output, - IOptionsSnapshot outputOptions, - IResultWriter resultWriter, - IObjectWriter objectWriter, - IUser user) : IRequestHandler -{ - public async Task Handle(TestImportVerb request, CancellationToken cancellationToken) - { - var filePath = request.FilePath ?? string.Empty; - var streamResult = await input.GetInput(filePath); - if (streamResult.IsFailed) - { - await resultWriter.Write(streamResult.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - if (!Guid.TryParse(request.BudgetId, out var budgetId)) - { - await resultWriter.Write(Result.Fail("Given ID is not a guid"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var budgets = await manager.GetOwnedBudgets(cancellationToken); - var budget = budgets.FirstOrDefault(b => b.Id == budgetId); - if (budget is null) - { - await resultWriter.Write(Result.Fail("Budget with given id does not exists"), cancellationToken); - return ExitCode.ArgumentsError; - } - - var settings = await settingsRepository.GetReadingOptionsFor(budget, cancellationToken); - var fileOptionsResult = settings.GetFileOptionsFor(filePath); - if (fileOptionsResult.IsFailed) - { - await resultWriter.Write(fileOptionsResult.ToResult(), cancellationToken); - return ExitCode.OperationError; - } - - var successes = new List(); - var errors = new List(); - - var ops = reader.ReadUnregisteredOperations(streamResult.Value, fileOptionsResult.Value, cancellationToken); - - await foreach (var result in ops) - { - if (result.IsSuccess) - { - successes.Add(CreateOperationFrom(result.Value)); - } - else - { - errors.Add(result.ToResult()); - } - } - - foreach (var error in errors) - { - await resultWriter.Write(error, cancellationToken); - } - - await objectWriter.Write(successes, cancellationToken); - - var writer = await output.GetOutput(outputOptions.Value.OutputStreamName); - await writer.WriteLineAsync(); - await writer.WriteLineAsync($"Total; {successes.Count + errors.Count}; Successes; {successes.Count}; Errors; {errors.Count}"); - await writer.FlushAsync(cancellationToken); - - return ExitCode.Success; - } - - private Operation CreateOperationFrom(UnregisteredOperation unregistered) - { - return new Operation( - Guid.Empty, - unregistered.Timestamp, - unregistered.Amount, - unregistered.Description, - new Domain.Entities.Accounts.Budget(Guid.Empty, "fake budget", [user.AsOwner()]), - [], - unregistered.Attributes - ); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestOperationStatsStatisticsVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestOperationStatsStatisticsVerb.cs deleted file mode 100644 index 58cfcada..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestOperationStatsStatisticsVerb.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Text; -using CommandLine; -using FluentResults; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Test; - -[Verb("ops-stats", HelpText = "Validates ruleset")] -internal class TestOperationStatsStatisticsVerb : AbstractVerb -{ - [Option('r', "ruleset", Required = true, HelpText = "Path to ruleset used for aggregation")] - public string Ruleset { get; set; } = ""; -} - - -internal class TestOperationStatsStatisticsVerbHandler( - ILogbookCriteriaReader reader, - IInputStreamProvider inputStreamProvider, - IOutputStreamProvider outputStreamProvider, - IOptionsSnapshot outputOptions, - IResultWriter writer -) : IRequestHandler -{ - public async Task Handle(TestOperationStatsStatisticsVerb request, CancellationToken cancellationToken) - { - var stream = await inputStreamProvider.GetInput(request.Ruleset); - if (!stream.IsSuccess) - { - await writer.Write(stream.ToResult(), cancellationToken); - return stream.ToExitCode(); - - } - - var criteria = await reader.ReadFrom(stream.Value, cancellationToken); - if (!criteria.IsSuccess) - { - await writer.Write(criteria.ToResult(), cancellationToken); - return criteria.ToExitCode(); - } - - var output = await outputStreamProvider.GetOutput(outputOptions.Value.OutputStreamName); - await WriteCriterion(output, criteria.Value, ""); - await output.FlushAsync(cancellationToken); - - return ExitCode.Success; - } - - private async Task WriteCriterion(StreamWriter output, LogbookCriteria criterion, string padding) - { - await output.WriteLineAsync($"{padding}Description: {criterion.Description}"); - if (criterion.Tags != null) - { - await output.WriteLineAsync($"{padding}Tags: [" + criterion.Tags.Aggregate(new StringBuilder(), (a, v) => a.Append($"{v.Value} ")) + "]"); - await output.WriteLineAsync($"{padding}Type: {criterion.Type}"); - } - else if (criterion.Substitution != null) - { - await output.WriteLineAsync($"{padding}Substitution: {criterion.Substitution}"); - } - else if (criterion.Criteria != null) - { - await output.WriteLineAsync($"{padding}Criteria: {criterion.Criteria}"); - } - - foreach (var subCriteria in criterion.Subcriteria ?? []) - { - await WriteCriterion(output, subCriteria, " " + padding); - } - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestVerb.cs deleted file mode 100644 index 2801024e..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Test/TestVerb.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Test; - -[Verb("test", HelpText = "Configuration testing")] -internal class TestVerb() : SuperVerb([typeof(TestImportVerb), typeof(TestOperationStatsStatisticsVerb)]); diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RegisterVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RegisterVerb.cs deleted file mode 100644 index 9323730e..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RegisterVerb.cs +++ /dev/null @@ -1,44 +0,0 @@ -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.UseCases.Transfers; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Transfers; - -[Verb("register", HelpText = "Register transfers manually")] -internal class RegisterVerb : AbstractVerb -{ - [Option('f', "file", HelpText = "Path to file with transfers to register. If value is not specified, app will use standard input")] - public string? FilePath { get; set; } -} - - -internal class RegisterVerbHandler(IMediator mediator, IInputStreamProvider streams, ITransfersReader reader, IResultWriter writer) : IRequestHandler -{ - public async Task Handle(RegisterVerb request, CancellationToken cancellationToken) - { - var steamReader = await streams.GetInput(request.FilePath ?? string.Empty); - if (!steamReader.IsSuccess) - { - await writer.Write(steamReader.ToResult(), cancellationToken); - return ExitCode.ArgumentsError; - } - - var exitCodes = new HashSet(); - var transfers = reader.ReadUnregisteredTransfers(steamReader.Value, cancellationToken).SelectAwait(async r => - { - if (r.IsSuccess) return r.Value; - await writer.Write(r.ToResult(), cancellationToken); - exitCodes.Add(r.ToExitCode()); - return null; - }).Where(o => o is not null); - - var result = await mediator.Send(new RegisterTransfersCommand(transfers!), cancellationToken); - exitCodes.Add(result.ToExitCode()); - - return exitCodes.Aggregate((r, e) => r | e); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RemoveTransfersVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RemoveTransfersVerb.cs deleted file mode 100644 index a4519757..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/RemoveTransfersVerb.cs +++ /dev/null @@ -1,37 +0,0 @@ -using CommandLine; -using FluentResults; -using MediatR; -using NVs.Budget.Application.Contracts.UseCases.Transfers; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Transfers; - -[Verb("remove", HelpText = "Remove tracked transfer")] -internal class RemoveTransfersVerb : AbstractVerb -{ - [Value(0, MetaName = "source id", HelpText = "Source Id of a transfer that should be removed")] - public IEnumerable? SourceIds { get; set; } - - [Option('a', "all", HelpText = "Remove all tracked transfers")] - public bool All {get; set;} -} - -internal class RemoveTransfersVerbHandler(IMediator mediator, IResultWriter writer) : IRequestHandler -{ - public async Task Handle(RemoveTransfersVerb request, CancellationToken cancellationToken) - { - var ids = (request.SourceIds ?? Enumerable.Empty()).ToList(); - if (ids.Count != 0 && request.All) - { - await writer.Write(Result.Fail(new Error("Please provide either list of ids or --all flag")), cancellationToken); - return ExitCode.ArgumentsError; - } - - - - var result = await mediator.Send(new RemoveTransfersCommand(ids.ToArray(), request.All), cancellationToken); - return result.ToExitCode(); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/SearchVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/SearchVerb.cs deleted file mode 100644 index cbc1a07d..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/SearchVerb.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Linq.Expressions; -using CommandLine; -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.Contracts.UseCases.Transfers; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Domain.Entities.Transactions; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = FluentResults.Error; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Transfers; - -[Verb("search", HelpText = "Search for transfers within a budget")] -internal class SearchVerb : CriteriaBasedVerb -{ - [Option('b', "budget-id", Required = false, HelpText = "ID of a budget. Optional if user has only one budget, otherwise required")] - public string BudgetId { get; [UsedImplicitly] set; } = string.Empty; - - [Option('c', "confidence", Required = false, HelpText = "Register found transfers that matches confidence level")] - public string? ConfidenceLevel { get; [UsedImplicitly] set; } -} - -internal class SearchVerbHandler(IMediator mediator, IBudgetManager manager, ICriteriaParser parser, IResultWriter writer, IObjectWriter transfersWriter) - : CriteriaBasedVerbHandler(parser, writer) -{ - protected override async Task HandleInternal(SearchVerb request, Expression> criteriaResultValue, CancellationToken ct) - { - DetectionAccuracy? accuracy = null; - if (request.ConfidenceLevel is not null) - { - if (Enum.TryParse(request.ConfidenceLevel, out DetectionAccuracy level)) - { - accuracy = level; - } - else - { - await Writer.Write(Result.Fail(new Error("Given apply is not a DetectionAccuracy!").WithMetadata("Value", request.ConfidenceLevel)), ct); - return ExitCode.ArgumentsError; - } - } - - TrackedBudget? budget; - var budgets = await manager.GetOwnedBudgets(ct); - if (string.IsNullOrEmpty(request.BudgetId) && budgets.Count == 1) - { - budget = budgets.Single(); - } - else - { - if (!Guid.TryParse(request.BudgetId, out var id)) - { - await Writer.Write(Result.Fail(new Error("Given budget id is not a guid").WithMetadata("Value", request.BudgetId)), ct); - return ExitCode.ArgumentsError; - - } - - budget = budgets.FirstOrDefault(b => b.Id == id); - - if (budget is null) - { - var fail = Result.Fail(new BudgetDoesNotExistError(id)); - await Writer.Write(fail, ct); - return fail.ToExitCode(); - } - } - - var found = await mediator.Send(new SearchTransfersCommand(budget, criteriaResultValue, accuracy), ct); - await transfersWriter.Write(found, request.OutputPath ?? string.Empty, ct); - - return ExitCode.Success; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/TransfersVerb.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/TransfersVerb.cs deleted file mode 100644 index bb02d28b..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Commands/Transfers/TransfersVerb.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Controllers.Console.Handlers.Commands.Transfers; - -[Verb("xfers", false, ["x"], HelpText = "Transfers management")] -internal class TransfersVerb() : SuperVerb([typeof(SearchVerb), typeof(RegisterVerb), typeof(RemoveTransfersVerb)]); diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/ConsoleControllersExtensions.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/ConsoleControllersExtensions.cs deleted file mode 100644 index 0bcfffdf..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/ConsoleControllersExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Globalization; -using System.Runtime.CompilerServices; -using CommandLine; -using MediatR; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Controllers.Console.Handlers.Behaviors; -using NVs.Budget.Controllers.Console.Handlers.Commands; -using NVs.Budget.Controllers.Console.Handlers.Utils; -using NVs.Budget.Utilities.MediatR; - -[assembly:InternalsVisibleTo("NVs.Budget.Controllers.Console.Handlers.Tests")] - -namespace NVs.Budget.Controllers.Console.Handlers; - -public static class ConsoleControllersExtensions -{ - public static IServiceCollection AddConsole(this IServiceCollection services) - { - services.AddMediatR(c => - { - c.RegisterServicesFromAssemblyContaining(); - c.AddOpenRequestPostProcessor(typeof(ResultWritingPostProcessor<,>)); - }); - services.AddTransient, SuperVerbHandler>(); - services.EmpowerMediatRHandlersFor(typeof(IRequestHandler<,>)); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - - return services; - } - - public static IServiceCollection UseConsole(this IServiceCollection services, IConfiguration configuration) - { - var cultureCode = configuration.GetValue("CultureCode"); - var culture = cultureCode is null ? CultureInfo.CurrentCulture : CultureInfo.GetCultureInfo(cultureCode); - - services.AddSingleton>(settings => - { - settings.AutoHelp = true; - settings.AutoVersion = true; - settings.CaseInsensitiveEnumValues = true; - settings.IgnoreUnknownArguments = true; - settings.ParsingCulture = culture; - settings.EnableDashDash = true; - }); - - return services; - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/EntryPoint.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/EntryPoint.cs deleted file mode 100644 index 7ba7a3e2..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/EntryPoint.cs +++ /dev/null @@ -1,109 +0,0 @@ -using CommandLine; -using CommandLine.Text; -using FluentResults; -using MediatR; -using Microsoft.Extensions.Options; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using Error = CommandLine.Error; - -namespace NVs.Budget.Controllers.Console.Handlers; - -internal class EntryPoint( - IMediator mediator, - Parser parser, - IOutputStreamProvider streams, - IOptionsSnapshot options, - IInputStreamProvider inputs, - IResultWriter resultWriter) : IEntryPoint -{ - private static readonly Type[] SuperVerbTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes().Where(t => t.IsAssignableTo(typeof(SuperVerb)))).ToArray(); - - public async Task Process(string[] args, CancellationToken ct) - { - if (args.Length == 0) - { - do - { - try - { - var reader = await inputs.GetInput(); - - if (reader.IsFailed) - { - await resultWriter.Write(reader.ToResult(), ct); - return (int)ExitCode.ArgumentsError; - } - - var output = await streams.GetOutput(options.Value.OutputStreamName); - await output.WriteAsync("> "); - await output.FlushAsync(ct); - - var line = await reader.Value.ReadLineAsync(ct); - if (ct.IsCancellationRequested) - { - return (int)ExitCode.Cancelled; - } - - args = line?.Split(' ').Where(s => !string.IsNullOrEmpty(s)).ToArray() ?? args; - await ProcessArgs(args, ct); - await streams.ReleaseStreamsAsync(); - await inputs.ReleaseStreamsAsync(); - } - catch(OperationCanceledException) - { - return (int)ExitCode.Cancelled; - } - catch (Exception e) - { - await resultWriter.Write(Result.Fail(new ExceptionalError(e)), ct); - return (int)ExitCode.UnexpectedResult; - } - - } while (!ct.IsCancellationRequested); - } - - return await ProcessArgs(args, ct); - } - - private async Task ProcessArgs(string[] args, CancellationToken ct) - { - var parsedResult = parser.ParseArguments(args, SuperVerbTypes); - return await parsedResult.MapResult(async obj => - { - //workaround to preserve options for subverbs - if (obj is SuperVerb request) - { - request.Args = args.Skip(1); - } - - var result = await mediator.Send(obj, ct); - if (result is int code) - { - return code; - } - - if (result is ExitCode exitCode) - { - return (int)exitCode; - } - - return (int)ExitCode.UnexpectedResult; - }, - async errs => - { - var helpText = HelpText.AutoBuild(parsedResult); - var errors = errs as Error[] ?? errs.ToArray(); - var isHelp = errors.IsHelp() || errors.IsVersion(); - var writer = isHelp - ? await streams.GetOutput(options.Value.OutputStreamName) - : await streams.GetError(options.Value.ErrorStreamName); - - await writer.WriteLineAsync(helpText); - await writer.FlushAsync(ct); - return (int)(isHelp ? ExitCode.Success : ExitCode.ArgumentsError); - }); - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/IEntryPoint.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/IEntryPoint.cs deleted file mode 100644 index 14546e70..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/IEntryPoint.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NVs.Budget.Controllers.Console.Handlers; - -public interface IEntryPoint -{ - Task Process(string[] args, CancellationToken ct); -} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/NVs.Budget.Controllers.Console.Handlers.csproj b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/NVs.Budget.Controllers.Console.Handlers.csproj deleted file mode 100644 index 6ef0046a..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/NVs.Budget.Controllers.Console.Handlers.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/EmptyRangeGivenError.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/EmptyRangeGivenError.cs deleted file mode 100644 index 7c1c3e0a..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/EmptyRangeGivenError.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Controllers.Console.Handlers.Utils; - -internal class EmptyRangeGivenError : IError -{ - public string Message => "Incorrect schedule given: no scheduled occurence belongs to given date range! At least 2 occurences required to "; - public Dictionary Metadata { get; } = new(); - public List Reasons { get; } = new(); -} \ No newline at end of file diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/IncorrectDateRangeGivenError.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/IncorrectDateRangeGivenError.cs deleted file mode 100644 index 5595ded9..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/IncorrectDateRangeGivenError.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Controllers.Console.Handlers.Utils; - -internal class IncorrectDateRangeGivenError : IError -{ - public string Message => "Incorrect date range given!"; - public Dictionary Metadata { get; } = new(); - public List Reasons { get; } = new(); -} \ No newline at end of file diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/NamedRangesBuilder.cs b/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/NamedRangesBuilder.cs deleted file mode 100644 index 8e359d55..00000000 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers/Utils/NamedRangesBuilder.cs +++ /dev/null @@ -1,45 +0,0 @@ -using FluentResults; -using NCrontab; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Controllers.Console.Handlers.Utils; - -internal class CronBasedNamedRangeSeriesBuilder -{ - public Result> GetRanges(DateTime from, DateTime till, string cronExpr) - { - if (till < from) - { - return Result.Fail(new IncorrectDateRangeGivenError()); - } - - CrontabSchedule schedule; - try - { - schedule = CrontabSchedule.Parse(cronExpr); - } - catch (Exception e) - { - return Result.Fail(new ExceptionBasedError(e)); - } - - var occurences = schedule.GetNextOccurrences(from.AddDays(-1), till.AddDays(1)).OrderBy(d => d).ToList(); - if (occurences.Count < 2) - { - return Result.Fail(new EmptyRangeGivenError()); - } - - return Result.Ok(GenerateRangesFrom(occurences)); - } - - private IEnumerable GenerateRangesFrom(List occurences) - { - var i = 1; - while (i < occurences.Count) - { - yield return new NamedRange(occurences[i - 1].ToString("dd/MM"), occurences[i - 1], occurences[i]); - i++; - } - } -} diff --git a/src/Controllers/NVs.Budget.Controllers.Web.Tests/BudgetMapperShould.cs b/src/Controllers/NVs.Budget.Controllers.Web.Tests/BudgetMapperShould.cs new file mode 100644 index 00000000..2958c233 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web.Tests/BudgetMapperShould.cs @@ -0,0 +1,536 @@ +using FluentAssertions; +using FluentResults.Extensions.FluentAssertions; +using NVs.Budget.Application.Contracts.Criteria; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Controllers.Web.Utils; +using NVs.Budget.Domain.Entities.Budgets; +using NVs.Budget.Domain.Entities.Operations; +using NVs.Budget.Utilities.Expressions; + +namespace NVs.Budget.Controllers.Web.Tests; + +public class BudgetMapperShould +{ + private readonly BudgetMapper _mapper; + private readonly ReadableExpressionsParser _parser; + + public BudgetMapperShould() + { + _parser = ReadableExpressionsParser.Default.RegisterAdditionalTypes( + typeof(TrackedOperation), + typeof(Operation) + ); + _mapper = new BudgetMapper(_parser); + } + + [Fact] + public void MapAllBudgetPropertiesToResponse() + { + // Arrange + var budgetId = Guid.NewGuid(); + var owner = new Owner(Guid.NewGuid(), "Test Owner"); + + var tagExpression = _parser.ParseUnaryConversion("o => \"Shopping\"").Value; + var conditionExpression = _parser.ParseUnaryPredicate("o => o.Amount.Amount > 100").Value; + var taggingCriterion = new TaggingCriterion(tagExpression, conditionExpression); + + var transferExpression = _parser.ParseBinaryPredicate( + "(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1" + ).Value; + var transferCriterion = new TransferCriterion(DetectionAccuracy.Exact, "Transfer", transferExpression); + + var logbookCriteria = LogbookCriteria.Universal; + + var budget = new TrackedBudget( + budgetId, + "Test Budget", + new[] { owner }, + new[] { taggingCriterion }, + new[] { transferCriterion }, + logbookCriteria) + { + Version = "v1" + }; + + // Act + var response = _mapper.ToResponse(budget); + + // Assert + response.Id.Should().Be(budgetId); + response.Name.Should().Be("Test Budget"); + response.Version.Should().Be("v1"); + response.Owners.Should().HaveCount(1); + response.Owners.First().Should().Be(owner); + response.TaggingCriteria.Should().HaveCount(1); + response.TransferCriteria.Should().HaveCount(1); + response.LogbookCriteria.Should().NotBeNull(); + } + + [Fact] + public void ConvertTaggingCriterionExpressionsToStrings() + { + // Arrange + var owner = new Owner(Guid.NewGuid(), "Test Owner"); + var tagExpression = _parser.ParseUnaryConversion("o => \"MyTag\"").Value; + var conditionExpression = _parser.ParseUnaryPredicate("o => o.Description.Contains(\"test\")").Value; + var taggingCriterion = new TaggingCriterion(tagExpression, conditionExpression); + + var budget = new TrackedBudget( + Guid.NewGuid(), + "Test Budget", + new[] { owner }, + new[] { taggingCriterion }, + Array.Empty(), + LogbookCriteria.Universal) + { + Version = "v1" + }; + + // Act + var response = _mapper.ToResponse(budget); + + // Assert + response.TaggingCriteria.Should().HaveCount(1); + var tagCriterion = response.TaggingCriteria.First(); + tagCriterion.Tag.Should().Be("o => \"MyTag\""); + tagCriterion.Condition.Should().Be("o => o.Description.Contains(\"test\")"); + } + + [Fact] + public void ConvertTransferCriterionToString() + { + // Arrange + var owner = new Owner(Guid.NewGuid(), "Test Owner"); + var transferExpression = _parser.ParseBinaryPredicate( + "(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1" + ).Value; + var transferCriterion = new TransferCriterion(DetectionAccuracy.Likely, "My Transfer", transferExpression); + + var budget = new TrackedBudget( + Guid.NewGuid(), + "Test Budget", + new[] { owner }, + Array.Empty(), + new[] { transferCriterion }, + LogbookCriteria.Universal) + { + Version = "v1" + }; + + // Act + var response = _mapper.ToResponse(budget); + + // Assert + response.TransferCriteria.Should().HaveCount(1); + var transferResp = response.TransferCriteria.First(); + transferResp.Accuracy.Should().Be("Likely"); + transferResp.Comment.Should().Be("My Transfer"); + transferResp.Criterion.Should().Be("(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1"); + } + + [Fact] + public void HandleEmptyVersionInResponse() + { + // Arrange + var owner = new Owner(Guid.NewGuid(), "Test Owner"); + var budget = new TrackedBudget( + Guid.NewGuid(), + "Test Budget", + new[] { owner }, + Array.Empty(), + Array.Empty(), + LogbookCriteria.Universal); + // Version is null + + // Act + var response = _mapper.ToResponse(budget); + + // Assert + response.Version.Should().Be(string.Empty); + } + + [Fact] + public void ParseValidTaggingCriterionExpressions() + { + // Arrange + var request = new TaggingCriterionResponse( + "o => \"Shopping\"", + "o => o.Amount.Amount > 100" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Tag.ToString().Should().Be("o => \"Shopping\""); + result.Value.Condition.ToString().Should().Be("o => o.Amount.Amount > 100"); + } + + [Fact] + public void ReturnErrorForInvalidTagExpression() + { + // Arrange + var request = new TaggingCriterionResponse( + "invalid expression", + "o => o.Amount.Amount > 100" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("does not match function format"); + } + + [Fact] + public void ReturnErrorForInvalidConditionExpression() + { + // Arrange + var request = new TaggingCriterionResponse( + "o => \"Shopping\"", + "not a valid expression" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("does not match function format"); + } + + [Fact] + public void ReturnErrorForInvalidPropertyAccessInTaggingCriterion() + { + // Arrange + var request = new TaggingCriterionResponse( + "o => o.NonExistentProperty", + "o => o.Amount.Amount > 100" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("Unable to create expression"); + } + + [Fact] + public void ParseValidTransferCriterionExpression() + { + // Arrange + var request = new TransferCriterionResponse( + "Exact", + "My Transfer", + "(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Accuracy.Should().Be(DetectionAccuracy.Exact); + result.Value.Comment.Should().Be("My Transfer"); + result.Value.Criterion.ToString().Should().Be("(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1"); + } + + [Fact] + public void ReturnErrorForInvalidDetectionAccuracy() + { + // Arrange + var request = new TransferCriterionResponse( + "InvalidAccuracy", + "My Transfer", + "(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("Invalid DetectionAccuracy value"); + } + + [Fact] + public void ReturnErrorForInvalidTransferExpression() + { + // Arrange + var request = new TransferCriterionResponse( + "Exact", + "My Transfer", + "not a valid binary expression" + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("does not match function format"); + } + + [Fact] + public void ParseUniversalLogbookCriteria() + { + // Arrange + var request = new LogbookCriteriaResponse( + "Universal", + null, + null, + null, + null, + null, + true + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Description.Should().Be("Universal"); + result.Value.IsUniversal.Should().BeTrue(); + } + + [Fact] + public void ParseLogbookCriteriaWithSubstitution() + { + // Arrange + var request = new LogbookCriteriaResponse( + "With Substitution", + null, + null, + null, + "o => o.Description", + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Description.Should().Be("With Substitution"); + result.Value.Substitution.Should().NotBeNull(); + result.Value.Substitution!.ToString().Should().Be("o => o.Description"); + } + + [Fact] + public void ParseLogbookCriteriaWithCriteria() + { + // Arrange + var request = new LogbookCriteriaResponse( + "With Criteria", + null, + null, + null, + null, + "o => o.Amount.Amount > 0", + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Description.Should().Be("With Criteria"); + result.Value.Criteria.Should().NotBeNull(); + result.Value.Criteria!.ToString().Should().Be("o => o.Amount.Amount > 0"); + } + + [Fact] + public void ParseLogbookCriteriaWithTags() + { + // Arrange + var request = new LogbookCriteriaResponse( + "With Tags", + null, + "OneOf", + new[] { "Tag1", "Tag2" }, + null, + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Description.Should().Be("With Tags"); + result.Value.Type.Should().Be(Domain.ValueObjects.Criteria.TagBasedCriterionType.OneOf); + result.Value.Tags.Should().HaveCount(2); + result.Value.Tags!.Select(t => t.Value).Should().Contain(new[] { "Tag1", "Tag2" }); + } + + [Fact] + public void ReturnErrorForInvalidTagBasedCriterionType() + { + // Arrange + var request = new LogbookCriteriaResponse( + "Invalid Type", + null, + "InvalidType", + new[] { "Tag1" }, + null, + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("Invalid TagBasedCriterionType value"); + } + + [Fact] + public void ReturnErrorForInvalidSubstitutionExpression() + { + // Arrange + var request = new LogbookCriteriaResponse( + "Invalid Substitution", + null, + null, + null, + "not a valid expression", + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("does not match function format"); + } + + [Fact] + public void ReturnErrorForInvalidCriteriaExpression() + { + // Arrange + var request = new LogbookCriteriaResponse( + "Invalid Criteria", + null, + null, + null, + null, + "not a valid predicate", + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + result.Errors.First().Message.Should().Contain("does not match function format"); + } + + [Fact] + public void ParseRecursiveLogbookSubcriteria() + { + // Arrange + var subCriterion1 = new LogbookCriteriaResponse("Sub1", null, null, null, null, null, true); + var subCriterion2 = new LogbookCriteriaResponse("Sub2", null, null, null, null, "o => o.Amount.Amount < 0", null); + + var request = new LogbookCriteriaResponse( + "Parent", + new[] { subCriterion1, subCriterion2 }, + null, + null, + null, + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeSuccess(); + result.Value.Description.Should().Be("Parent"); + result.Value.Subcriteria.Should().HaveCount(2); + result.Value.Subcriteria!.ElementAt(0).Description.Should().Be("Sub1"); + result.Value.Subcriteria!.ElementAt(1).Description.Should().Be("Sub2"); + } + + [Fact] + public void PropagateErrorsFromInvalidSubcriteria() + { + // Arrange + var invalidSubCriterion = new LogbookCriteriaResponse( + "Invalid Sub", + null, + null, + null, + null, + "invalid expression", + null + ); + + var request = new LogbookCriteriaResponse( + "Parent", + new[] { invalidSubCriterion }, + null, + null, + null, + null, + null + ); + + // Act + var result = _mapper.FromRequest(request); + + // Assert + result.Should().BeFailure(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void PreserveExpressionsInRoundTripConversion() + { + // Arrange + var originalExpression = "o => o.Description.Contains(\"grocery\")"; + var request = new TaggingCriterionResponse("o => \"Food\"", originalExpression); + + // Act + var parseResult = _mapper.FromRequest(request); + var criterion = parseResult.Value; + + // Convert back to response + var owner = new Owner(Guid.NewGuid(), "Test"); + var budget = new TrackedBudget( + Guid.NewGuid(), + "Test", + new[] { owner }, + new[] { criterion }, + Array.Empty(), + LogbookCriteria.Universal + ) { Version = "v1" }; + + var response = _mapper.ToResponse(budget); + + // Assert + parseResult.Should().BeSuccess(); + response.TaggingCriteria.First().Condition.Should().Be(originalExpression); + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/GlobalUsings.cs b/src/Controllers/NVs.Budget.Controllers.Web.Tests/GlobalUsings.cs similarity index 100% rename from src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/GlobalUsings.cs rename to src/Controllers/NVs.Budget.Controllers.Web.Tests/GlobalUsings.cs diff --git a/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/NVs.Budget.Controllers.Console.Handlers.Tests.csproj b/src/Controllers/NVs.Budget.Controllers.Web.Tests/NVs.Budget.Controllers.Web.Tests.csproj similarity index 72% rename from src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/NVs.Budget.Controllers.Console.Handlers.Tests.csproj rename to src/Controllers/NVs.Budget.Controllers.Web.Tests/NVs.Budget.Controllers.Web.Tests.csproj index ef1c5077..30995166 100644 --- a/src/Controllers/NVs.Budget.Controllers.Console.Handlers.Tests/NVs.Budget.Controllers.Console.Handlers.Tests.csproj +++ b/src/Controllers/NVs.Budget.Controllers.Web.Tests/NVs.Budget.Controllers.Web.Tests.csproj @@ -13,20 +13,22 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + + diff --git a/src/Controllers/NVs.Budget.Controllers.Web.Tests/YamlDeserializationShould.cs b/src/Controllers/NVs.Budget.Controllers.Web.Tests/YamlDeserializationShould.cs new file mode 100644 index 00000000..4c0b43a4 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web.Tests/YamlDeserializationShould.cs @@ -0,0 +1,202 @@ +using FluentAssertions; +using NVs.Budget.Controllers.Web.Models; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace NVs.Budget.Controllers.Web.Tests; + +public class YamlDeserializationShould +{ + private readonly IDeserializer _deserializer; + + public YamlDeserializationShould() + { + _deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + } + + [Fact] + public void DeserializeUpdateBudgetRequestFromYaml() + { + // Arrange + var yaml = @" +name: Test Budget +version: v1.0 +taggingCriteria: + - tag: o => o.Description + condition: o => o.Amount.Amount > 0 +transferCriteria: + - accuracy: Exact + comment: Transfer + criterion: (source, sink) => source.Amount.Amount == sink.Amount.Amount * -1 +logbookCriteria: + description: Main criteria + isUniversal: false + type: Any + tags: + - income + - expense + substitution: o => o.Description + subcriteria: + - description: Income subcriteria + isUniversal: true +"; + + // Act + var result = _deserializer.Deserialize(yaml); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be("Test Budget"); + result.Version.Should().Be("v1.0"); + + result.TaggingCriteria.Should().NotBeNull(); + result.TaggingCriteria.Should().HaveCount(1); + var taggingCriterionResponse = result.TaggingCriteria!.First(); + taggingCriterionResponse.Tag.Should().Be("o => o.Description"); + taggingCriterionResponse.Condition.Should().Be("o => o.Amount.Amount > 0"); + + result.TransferCriteria.Should().NotBeNull(); + result.TransferCriteria.Should().HaveCount(1); + var criterionResponse = result.TransferCriteria!.First(); + criterionResponse.Accuracy.Should().Be("Exact"); + criterionResponse.Comment.Should().Be("Transfer"); + + result.LogbookCriteria.Should().NotBeNull(); + result.LogbookCriteria!.Description.Should().Be("Main criteria"); + result.LogbookCriteria.IsUniversal.Should().BeFalse(); + result.LogbookCriteria.Type.Should().Be("Any"); + result.LogbookCriteria.Tags.Should().Contain(new[] { "income", "expense" }); + result.LogbookCriteria.Subcriteria.Should().HaveCount(1); + var criteriaResponse = result.LogbookCriteria.Subcriteria!.First(); + criteriaResponse.Description.Should().Be("Income subcriteria"); + criteriaResponse.IsUniversal.Should().BeTrue(); + } + + [Fact] + public void DeserializeUpdateBudgetRequestWithEmptyCollections() + { + // Arrange + var yaml = @" +name: Simple Budget +version: v1.0 +logbookCriteria: + description: Universal + isUniversal: true +"; + + // Act + var result = _deserializer.Deserialize(yaml); + + // Assert + result.Should().NotBeNull(); + result.Name.Should().Be("Simple Budget"); + result.Version.Should().Be("v1.0"); + result.TaggingCriteria.Should().BeNullOrEmpty(); + result.TransferCriteria.Should().BeNullOrEmpty(); + result.LogbookCriteria.Should().NotBeNull(); + result.LogbookCriteria!.IsUniversal.Should().BeTrue(); + } + + [Fact] + public void DeserializeTaggingCriterionResponse() + { + // Arrange + var yaml = @" +tag: o => o.Description +condition: o => o.Amount.Amount > 0 +"; + + // Act + var result = _deserializer.Deserialize(yaml); + + // Assert + result.Should().NotBeNull(); + result.Tag.Should().Be("o => o.Description"); + result.Condition.Should().Be("o => o.Amount.Amount > 0"); + } + + [Fact] + public void DeserializeTransferCriterionResponse() + { + // Arrange + var yaml = @" +accuracy: Exact +comment: Test transfer +criterion: (source, sink) => source.Amount.Amount == sink.Amount.Amount * -1 +"; + + // Act + var result = _deserializer.Deserialize(yaml); + + // Assert + result.Should().NotBeNull(); + result.Accuracy.Should().Be("Exact"); + result.Comment.Should().Be("Test transfer"); + result.Criterion.Should().Be("(source, sink) => source.Amount.Amount == sink.Amount.Amount * -1"); + } + + [Fact] + public void FailToDeserializeInvalidYaml() + { + // Arrange + var invalidYaml = @" +name: Test Budget +version: v1.0 +taggingCriteria: + - this is not valid yaml structure + missing proper formatting +"; + + // Act & Assert + var exception = Assert.Throws(() => + { + _deserializer.Deserialize(invalidYaml); + }); + + exception.Should().NotBeNull(); + } + + [Fact] + public void DeserializeLogbookCriteriaResponseWithNestedSubcriteria() + { + // Arrange + var yaml = @" +description: Root criteria +type: Any +tags: + - tag1 + - tag2 +substitution: o => o.Description +subcriteria: + - description: Level 1 + isUniversal: true + subcriteria: + - description: Level 2 + criteria: o => o.Amount.Amount > 0 +"; + + // Act + var result = _deserializer.Deserialize(yaml); + + // Assert + result.Should().NotBeNull(); + result.Description.Should().Be("Root criteria"); + result.Type.Should().Be("Any"); + result.Tags.Should().Contain(new[] { "tag1", "tag2" }); + result.Substitution.Should().Be("o => o.Description"); + + result.Subcriteria.Should().HaveCount(1); + var criteriaResponse = result.Subcriteria!.First(); + criteriaResponse.Description.Should().Be("Level 1"); + criteriaResponse.IsUniversal.Should().BeTrue(); + + criteriaResponse.Subcriteria.Should().HaveCount(1); + var subcriteriaResponse = criteriaResponse.Subcriteria!.First(); + subcriteriaResponse.Description.Should().Be("Level 2"); + subcriteriaResponse.Criteria.Should().Be("o => o.Amount.Amount > 0"); + } + +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Controllers/BudgetController.cs b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/BudgetController.cs new file mode 100644 index 00000000..c54a62b3 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/BudgetController.cs @@ -0,0 +1,386 @@ +using Asp.Versioning; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NVs.Budget.Application.Contracts.Criteria; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Application.Contracts.UseCases.Budgets; +using NVs.Budget.Application.Contracts.UseCases.Owners; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Controllers.Web.Utils; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Controllers.Web.Controllers; + +[Authorize] +[ApiVersion("0.1")] +[Route("api/v{version:apiVersion}/[controller]")] +[Produces("application/json", "application/yaml", "text/yaml")] +public class BudgetController( + IMediator mediator, + BudgetMapper mapper, + IReadingSettingsRepository readingSettingsRepository, + FileReadingSettingsMapper settingsMapper) : Controller +{ + /// + /// Gets all budgets available to the current user (owned by or shared with the user) + /// + /// Cancellation token + /// Collection of available budgets + [HttpGet] + [ProducesResponseType(typeof(IReadOnlyCollection), 200)] + public async Task> GetAvailableBudgets(CancellationToken ct) + { + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + return budgets.Select(mapper.ToResponse).ToList(); + } + + /// + /// Gets a specific budget by ID + /// + /// Budget ID + /// Cancellation token + /// Budget details or 404 if not found + [HttpGet("{id}")] + [ProducesResponseType(typeof(BudgetResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task GetBudgetById(Guid id, CancellationToken ct) + { + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == id); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {id} not found or access denied") }); + } + + return Ok(mapper.ToResponse(budget)); + } + + /// + /// Registers a new budget + /// + /// Budget registration request + /// Cancellation token + /// Created budget or error details + [HttpPost] + [ProducesResponseType(typeof(BudgetResponse), 201)] + [ProducesResponseType(typeof(IEnumerable), 400)] + public async Task RegisterBudget([FromBody] RegisterBudgetRequest request, CancellationToken ct) + { + var command = new RegisterBudgetCommand(new UnregisteredBudget(request.Name)); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + var response = mapper.ToResponse(result.Value); + return CreatedAtAction(nameof(GetAvailableBudgets), new { id = result.Value.Id }, response); + } + + return BadRequest(result.Errors); + } + + /// + /// Changes the owners of a budget + /// + /// Change owners request + /// Cancellation token + /// Success or error details + [HttpPut("owners")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task ChangeBudgetOwners( + [FromBody] ChangeBudgetOwnersRequest request, + CancellationToken ct) + { + // First, get the budget to validate it exists and user has access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == request.Budget.Id); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {request.Budget.Id} not found or access denied") }); + } + + // Fetch actual owners by their IDs + var owners = await mediator.Send(new ListOwnersQuery(o => request.OwnerIds.Contains(o.Id)), ct); + + if (owners.Count != request.OwnerIds.Count) + { + var foundIds = owners.Select(o => o.Id).ToList(); + var missingIds = request.OwnerIds.Except(foundIds).ToList(); + return BadRequest(new List { new($"Owners not found: {string.Join(", ", missingIds)}") }); + } + + // Create budget with user-provided version + var budgetToUpdate = new TrackedBudget( + request.Budget.Id, + budget.Name, + owners, + budget.TaggingCriteria, + budget.TransferCriteria, + budget.LogbookCriteria) + { + Version = request.Budget.Version + }; + + var command = new ChangeBudgetOwnersCommand(budgetToUpdate, owners); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } + + /// + /// Updates a budget + /// + /// Budget ID + /// Budget update request + /// Cancellation token + /// Success or error details + [HttpPut("{id:guid}")] + [ProducesResponseType(204)] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task UpdateBudget( + [FromRoute] Guid id, + [FromBody] UpdateBudgetRequest request, + CancellationToken ct) + { + // First, get the budget to validate it exists and user has access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == id); + + if (budget == null) + { + return NotFound(new List { new Error($"Budget with ID {id} not found or access denied") }); + } + + // Parse tagging criteria if provided + List taggingCriteria = budget.TaggingCriteria.ToList(); + if (request.TaggingCriteria != null && request.TaggingCriteria.Any()) + { + taggingCriteria = new List(); + foreach (var tc in request.TaggingCriteria) + { + if (tc == null) continue; // Skip null items + + var parseResult = mapper.FromRequest(tc); + if (parseResult.IsFailed) + { + return BadRequest(parseResult.Errors); + } + taggingCriteria.Add(parseResult.Value); + } + } + + // Parse transfer criteria if provided + List transferCriteria = budget.TransferCriteria.ToList(); + if (request.TransferCriteria != null && request.TransferCriteria.Any()) + { + transferCriteria = new List(); + foreach (var tc in request.TransferCriteria) + { + if (tc == null) continue; // Skip null items + + var parseResult = mapper.FromRequest(tc); + if (parseResult.IsFailed) + { + return BadRequest(parseResult.Errors); + } + transferCriteria.Add(parseResult.Value); + } + } + + // Parse logbook criteria if provided + LogbookCriteria logbookCriteria = budget.LogbookCriteria; + if (request.LogbookCriteria != null) + { + var parseResult = mapper.FromRequest(request.LogbookCriteria); + if (parseResult.IsFailed) + { + return BadRequest(parseResult.Errors); + } + logbookCriteria = parseResult.Value; + } + + // Create updated budget with new properties using user-provided version + var updatedBudget = new TrackedBudget( + id, + request.Name, + budget.Owners, + taggingCriteria, + transferCriteria, + logbookCriteria) + { + Version = request.Version + }; + + var command = new UpdateBudgetCommand(updatedBudget); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } + + /// + /// Removes a budget + /// + /// Budget ID + /// Budget version for optimistic concurrency + /// Cancellation token + /// Success or error details + [HttpDelete("{id:guid}")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task RemoveBudget( + [FromRoute] Guid id, + [FromQuery] string version, + CancellationToken ct) + { + // First, get the budget to validate it exists and user has access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == id); + + if (budget == null) + { + return NotFound(new List { new Error($"Budget with ID {id} not found or access denied") }); + } + + // Create budget with user-provided version + var budgetToRemove = new TrackedBudget( + id, + budget.Name, + budget.Owners, + budget.TaggingCriteria, + budget.TransferCriteria, + budget.LogbookCriteria) + { + Version = version + }; + + var command = new RemoveBudgetCommand(budgetToRemove); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } + + /// + /// Merges multiple budgets into one + /// + /// Merge budgets request + /// Cancellation token + /// Success or error details + [HttpPost("merge")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(IEnumerable), 400)] + public async Task MergeBudgets([FromBody] MergeBudgetsRequest request, CancellationToken ct) + { + var mergeRequest = new NVs.Budget.Application.Contracts.UseCases.Budgets.MergeBudgetsRequest(request.BudgetIds.ToList(), request.PurgeEmptyBudgets); + var result = await mediator.Send(mergeRequest, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } + + /// + /// Gets file reading settings for a specific budget + /// + /// Budget ID + /// Cancellation token + /// Reading settings or error details + [HttpGet("{id:guid}/reading-settings")] + [ProducesResponseType(typeof(Dictionary), 200)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task GetReadingSettings(Guid id, CancellationToken ct) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == id); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {id} not found or access denied") }); + } + + // Get reading settings + var settings = await readingSettingsRepository.GetReadingSettingsFor(budget, ct); + var response = settingsMapper.ToResponse(settings); + + return Ok(response); + } + + /// + /// Updates file reading settings for a specific budget + /// + /// Budget ID + /// Reading settings update request (dictionary of pattern to settings) + /// Cancellation token + /// Success or error details + [HttpPut("{id:guid}/reading-settings")] + [ProducesResponseType(204)] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task UpdateReadingSettings( + [FromRoute] Guid id, + [FromBody] Dictionary request, + CancellationToken ct) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == id); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {id} not found or access denied") }); + } + + // Parse request + var parseResult = settingsMapper.FromRequest(request); + if (parseResult.IsFailed) + { + return BadRequest(parseResult.Errors); + } + + // Update settings + var result = await readingSettingsRepository.UpdateReadingSettingsFor(budget, parseResult.Value, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } +} + +// Request models for the controller +public record BudgetIdentifier(Guid Id, string Version); + +public record RegisterBudgetRequest(string Name); + +public record ChangeBudgetOwnersRequest(BudgetIdentifier Budget, IReadOnlyCollection OwnerIds); + +public record MergeBudgetsRequest(IReadOnlyCollection BudgetIds, bool PurgeEmptyBudgets); diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OperationsController.cs b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OperationsController.cs new file mode 100644 index 00000000..5b3a8bac --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OperationsController.cs @@ -0,0 +1,572 @@ +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Asp.Versioning; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NVs.Budget.Application.Contracts.Options; +using NVs.Budget.Application.Contracts.Queries; +using NVs.Budget.Application.Contracts.UseCases.Budgets; +using NVs.Budget.Application.Contracts.UseCases.Operations; +using System.Linq; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Exceptions; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Controllers.Web.Utils; +using NVs.Budget.Domain.Aggregates; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; +using NVs.Budget.Utilities.Expressions; + +namespace NVs.Budget.Controllers.Web.Controllers; + +[Authorize] +[ApiVersion("0.1")] +[Route("api/v{version:apiVersion}/budget/{budgetId:guid}/[controller]")] +[Produces("application/json", "application/yaml", "text/yaml")] +public class OperationsController( + IMediator mediator, + OperationMapper mapper, + LogbookMapper logbookMapper, + ReadableExpressionsParser parser, + ICsvFileReader csvReader, + IReadingSettingsRepository settingsRepository, + RangeBuilder rangeBuilder) : Controller +{ + /// + /// Gets all operations for a specific budget + /// + /// Budget ID to filter operations + /// Optional filter criteria expression + /// Optional output currency for conversion + /// Whether to exclude transfers from results + /// Cancellation token + /// Collection of operations + [HttpGet("duplicates")] + [ProducesResponseType(typeof(IReadOnlyCollection>), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task GetDuplicates( + [FromRoute] Guid budgetId, + [FromQuery] string? criteria = null, + CancellationToken ct = default) + { + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + throw new NotFoundException($"Budget with ID {budgetId} not found or access denied"); + } + + Expression>? conditions = null; + + if (!string.IsNullOrWhiteSpace(criteria)) + { + var criteriaResult = parser.ParseUnaryPredicate(criteria); + if (criteriaResult.IsFailed) + { + throw new BadRequestException($"Invalid criteria: {string.Join("; ", criteriaResult.Errors.Select(e => e.Message))}"); + } + conditions = criteriaResult.Value.AsExpression(); + } + else + { + // Default: all operations + conditions = o => true; + } + + var query = new ListDuplicatedOperationsQuery(conditions); + var duplicateGroups = await mediator.Send(query, ct); + + var response = duplicateGroups + .Select(group => group.Select(mapper.ToResponse).ToList()) + .OrderHistorically() + .ToList(); + + return Ok(response); + } + + [HttpGet] + [ProducesResponseType(typeof(IAsyncEnumerable), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async IAsyncEnumerable GetOperations( + [FromRoute] Guid budgetId, + [FromQuery] string? criteria = null, + [FromQuery] string? outputCurrency = null, + [FromQuery] bool excludeTransfers = false, + [EnumeratorCancellation] CancellationToken ct = default) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + throw new NotFoundException($"Budget with ID {budgetId} not found or access denied"); + } + + // Parse user-provided criteria if specified + Expression>? conditions = null; + if (!string.IsNullOrWhiteSpace(criteria)) + { + var criteriaResult = parser.ParseUnaryPredicate(criteria); + if (criteriaResult.IsFailed) + { + throw new BadRequestException(criteriaResult.Errors); + } + + conditions = criteriaResult.Value.AsExpression(); + } + + // Parse output currency if provided + NMoneys.Currency? currency = null; + if (!string.IsNullOrWhiteSpace(outputCurrency)) + { + try + { + currency = NMoneys.Currency.Get(outputCurrency); + } + catch (Exception ex) + { + throw new BadRequestException($"Invalid currency code: {outputCurrency}. {ex.Message}"); + } + } + + var query = new OperationQuery(conditions, currency, excludeTransfers); + var listQuery = new ListOperationsQuery(query); + + await foreach (var operation in mediator.CreateStream(listQuery, ct).OrderHistorically()) + { + yield return mapper.ToResponse(operation); + } + } + + /// + /// Imports new operations into a budget from CSV file + /// + /// Budget ID from route + /// CSV file to import + /// Budget version for optimistic concurrency + /// Optional transfer detection confidence level + /// Optional file pattern to match reading settings (default: .*) + /// Cancellation token + /// Import result with success/failure details + [HttpPost("import")] + [Consumes("multipart/form-data")] + [ProducesResponseType(typeof(ImportResultResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task ImportOperations( + [FromRoute] Guid budgetId, + IFormFile file, + [FromForm] string budgetVersion, + [FromForm] string? transferConfidenceLevel = null, + [FromForm] string? filePattern = null, + CancellationToken ct = default) + { + // Validate file + if (file == null || file.Length == 0) + { + return BadRequest(new List { new("No file uploaded") }); + } + + if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) + { + return BadRequest(new List { new("Only CSV files are supported") }); + } + + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + throw new NotFoundException($"Budget with ID {budgetId} not found or access denied"); + } + + // Get reading settings for this budget + var allSettings = await settingsRepository.GetReadingSettingsFor(budget, ct); + + // Find matching setting by file pattern + var pattern = filePattern ?? ".*"; + var regex = new Regex(pattern); + FileReadingSetting? readingSetting = allSettings + .Where(kvp => kvp.Key.IsMatch(file.FileName)) + .Select(kvp => kvp.Value) + .FirstOrDefault(); + + if (readingSetting == null) + { + // Try to match the provided pattern + readingSetting = allSettings + .Where(kvp => kvp.Key.ToString() == pattern) + .Select(kvp => kvp.Value) + .FirstOrDefault(); + } + + if (readingSetting == null) + { + return BadRequest(new List { new($"No reading settings found for file pattern '{pattern}'. Please configure reading settings for this budget first.") }); + } + + // Parse transfer confidence level + DetectionAccuracy? transferAccuracy = null; + if (!string.IsNullOrWhiteSpace(transferConfidenceLevel)) + { + var accuracyResult = mapper.ParseDetectionAccuracy(transferConfidenceLevel); + if (accuracyResult.IsFailed) + { + return BadRequest(accuracyResult.Errors); + } + transferAccuracy = accuracyResult.Value; + } + + // Update budget version for optimistic concurrency + budget.Version = budgetVersion; + + // Read and parse CSV file + var parseErrors = new List(); + async IAsyncEnumerable ReadOperationsAsync() + { + using var stream = file.OpenReadStream(); + using var reader = new StreamReader(stream, readingSetting.Encoding); + + await foreach (var result in csvReader.ReadUntrackedOperations(reader, readingSetting, ct)) + { + if (result.IsSuccess) + { + yield return result.Value; + } + else + { + // Collect parsing errors + parseErrors.AddRange(result.Errors); + } + } + } + + var options = new ImportOptions(transferAccuracy); + var command = new ImportOperationsCommand( + ReadOperationsAsync(), + budget, + options + ); + + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + // Combine parsing errors with import reasons + var importErrors = result.Reasons.Where(r => r is IError).Cast().ToList(); + var allErrors = parseErrors.Concat(importErrors).ToList(); + + var allSuccesses = result.Reasons + .Where(r => r is ISuccess) + .Cast() + .ToList(); + + var response = new ImportResultResponse( + result.Operations.Select(mapper.ToResponse).OrderHistorically().ToList(), + result.Duplicates.Select(group => group.Select(mapper.ToResponse).ToList()).OrderHistorically().ToList(), + allErrors, + allSuccesses + ); + return Ok(response); + } + + return BadRequest(result.Errors); + } + + /// + /// Updates existing operations in a budget + /// + /// Budget ID from route + /// Update operations request + /// Cancellation token + /// Update result with success/failure details + [HttpPut] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(typeof(UpdateResultResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task UpdateOperations( + [FromRoute] Guid budgetId, + [FromBody] UpdateOperationsRequest request, + CancellationToken ct) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Update budget version for optimistic concurrency + budget.Version = request.BudgetVersion; + + // Parse operations + var operations = new List(); + foreach (var op in request.Operations) + { + var parseResult = mapper.FromRequest(op, budget); + if (parseResult.IsFailed) + { + return BadRequest(parseResult.Errors); + } + operations.Add(parseResult.Value); + } + + // Parse transfer confidence level + DetectionAccuracy? transferConfidenceLevel = null; + if (!string.IsNullOrWhiteSpace(request.TransferConfidenceLevel)) + { + var accuracyResult = mapper.ParseDetectionAccuracy(request.TransferConfidenceLevel); + if (accuracyResult.IsFailed) + { + return BadRequest(accuracyResult.Errors); + } + transferConfidenceLevel = accuracyResult.Value; + } + + // Parse tagging mode + if (!Enum.TryParse(request.TaggingMode, true, out var taggingMode)) + { + return BadRequest(new List { new($"Invalid TaggingMode value: {request.TaggingMode}") }); + } + + var options = new UpdateOptions(transferConfidenceLevel, taggingMode); + + async IAsyncEnumerable GetOperationsAsync() + { + foreach (var op in operations) + { + yield return op; + } + await Task.CompletedTask; + } + + var command = new UpdateOperationsCommand( + GetOperationsAsync(), + budget, + options + ); + + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + var errors = result.Reasons.Where(r => r is IError).Cast().ToList(); + var successes = result.Reasons.Where(r => r is ISuccess).Cast().ToList(); + + var response = new UpdateResultResponse( + result.Operations.Select(mapper.ToResponse).OrderHistorically().ToList(), + errors, + successes + ); + return Ok(response); + } + + return BadRequest(result.Errors); + } + + /// + /// Removes operations matching the specified criteria from a specific budget + /// + /// Budget ID from route + /// Remove operations request with criteria expression + /// Cancellation token + /// Delete result with success/failure details + [HttpDelete] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(typeof(DeleteResultResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task RemoveOperations( + [FromRoute] Guid budgetId, + [FromBody] RemoveOperationsRequest request, + CancellationToken ct) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Parse criteria expression + var criteriaResult = parser.ParseUnaryPredicate(request.Criteria); + if (criteriaResult.IsFailed) + { + return BadRequest(criteriaResult.Errors); + } + + var command = new RemoveOperationsCommand(criteriaResult.Value.AsExpression()); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + var response = new DeleteResultResponse(result.Errors, result.Successes); + return Ok(response); + } + + return BadRequest(result.Errors); + } + + /// + /// Retags operations matching the specified criteria in a specific budget + /// + /// Budget ID from route + /// Retag operations request with criteria expression and options + /// Cancellation token + /// Retag result with success/failure details + [HttpPost("retag")] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(typeof(RetagResultResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task RetagOperations( + [FromRoute] Guid budgetId, + [FromBody] RetagOperationsRequest request, + CancellationToken ct) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Update budget version for optimistic concurrency + budget.Version = request.BudgetVersion; + + // Parse criteria expression + var criteriaResult = parser.ParseUnaryPredicate(request.Criteria); + if (criteriaResult.IsFailed) + { + return BadRequest(criteriaResult.Errors); + } + + var command = new RetagOperationsCommand(criteriaResult.Value.AsExpression(), budget, request.FromScratch); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + var response = new RetagResultResponse(result.Errors, result.Successes); + return Ok(response); + } + + return BadRequest(result.Errors); + } + + /// + /// Get aggregated operations statistics (logbook) for a specific budget + /// + /// Budget ID from route + /// Start date for filtering operations + /// End date for filtering operations + /// Optional additional filter criteria expression + /// Optional cron expression to divide logbook into ranges + /// Cancellation token + /// Logbook with aggregated statistics divided by ranges + [HttpGet("logbook")] + [ProducesResponseType(typeof(LogbookResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task GetLogbook( + [FromRoute] Guid budgetId, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? till = null, + [FromQuery] string? criteria = null, + [FromQuery] string? cronExpression = null, + CancellationToken ct = default) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Parse user criteria first + Expression>? userCriteria = null; + if (!string.IsNullOrWhiteSpace(criteria)) + { + var criteriaResult = parser.ParseUnaryPredicate(criteria); + if (criteriaResult.IsFailed) + { + return BadRequest(criteriaResult.Errors); + } + userCriteria = criteriaResult.Value.AsExpression(); + } + + // Build filter expression using CombineWith + Expression> filter = o => o.Budget.Id == budget.Id; + + if (from.HasValue) + { + var fromUtc = from.Value.ToUniversalTime(); + filter = filter.CombineWith(o => o.Timestamp >= fromUtc); + } + + if (till.HasValue) + { + var tillUtc = till.Value.ToUniversalTime(); + filter = filter.CombineWith(o => o.Timestamp < tillUtc); + } + + if (userCriteria != null) + { + filter = filter.CombineWith(userCriteria); + } + + // Execute query + var query = new CalcOperationsStatisticsQuery(budget.LogbookCriteria.GetCriterion(), filter); + var result = await mediator.Send(query, ct); + + if (result.IsFailed) + { + return BadRequest(result.Errors); + } + + // Generate ranges + var fromDate = from ?? result.Value.From; + var tillDate = till ?? result.Value.Till; + + var rangesResult = rangeBuilder.GetRanges(fromDate, tillDate, cronExpression); + if (rangesResult.IsFailed) + { + return BadRequest(rangesResult.Errors); + } + + // Map to response with ranges + var rangedEntries = rangesResult.Value + .Select(range => + { + var rangedLogbook = (CriteriaBasedLogbook)result.Value[range.From, range.Till]; + var entry = logbookMapper.ToResponse(rangedLogbook); + var rangeResponse = new NamedRangeResponse(range.Name, range.From, range.Till); + return new RangedLogbookEntryResponse(rangeResponse, entry); + }) + .ToList(); + + var response = new LogbookResponse( + rangedEntries, + result.Errors, + result.Successes + ); + + return Ok(response); + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OwnersController.cs b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OwnersController.cs new file mode 100644 index 00000000..f5eba6a8 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/OwnersController.cs @@ -0,0 +1,16 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NVs.Budget.Domain.Entities.Budgets; +using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; + +namespace NVs.Budget.Controllers.Web.Controllers; + +[Authorize] +[ApiVersion("0.1")] +[Route("api/v{version:apiVersion}/[controller]")] +public class OwnersController(IOwnersRepository owners) : Controller +{ + [HttpGet] + public async Task> Get(CancellationToken ct) => await owners.Get(o => true, ct); +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Controllers/TransfersController.cs b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/TransfersController.cs new file mode 100644 index 00000000..1951a134 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Controllers/TransfersController.cs @@ -0,0 +1,212 @@ +using Asp.Versioning; +using FluentResults; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Application.Contracts.Queries; +using NVs.Budget.Application.Contracts.UseCases.Budgets; +using NVs.Budget.Application.Contracts.UseCases.Operations; +using NVs.Budget.Application.Contracts.UseCases.Transfers; +using NVs.Budget.Controllers.Web.Exceptions; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Controllers.Web.Utils; + +namespace NVs.Budget.Controllers.Web.Controllers; + +[Authorize] +[ApiVersion("0.1")] +[Route("api/v{version:apiVersion}/budget/{budgetId:guid}/[controller]")] +[Produces("application/json", "application/yaml", "text/yaml")] +public class TransfersController( + IMediator mediator, + TransferMapper mapper, + OperationMapper operationMapper) : Controller +{ + /// + /// Searches for transfers in a specific budget + /// + /// Budget ID from route + /// Start date for filtering transfers + /// End date for filtering transfers + /// Optional detection accuracy filter + /// Cancellation token + /// Collection of transfers (both recorded and unregistered) + [HttpGet] + [ProducesResponseType(typeof(TransfersListResponse), 200)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task SearchTransfers( + [FromRoute] Guid budgetId, + [FromQuery] DateTime? from = null, + [FromQuery] DateTime? till = null, + [FromQuery] string? accuracy = null, + CancellationToken ct = default) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Set default dates if not provided + var fromDate = from ?? DateTime.UtcNow.AddMonths(-1); + var tillDate = till ?? DateTime.UtcNow; + + // Parse accuracy if provided + DetectionAccuracy? detectionAccuracy = null; + if (!string.IsNullOrWhiteSpace(accuracy)) + { + var accuracyResult = operationMapper.ParseDetectionAccuracy(accuracy); + if (accuracyResult.IsFailed) + { + return BadRequest(accuracyResult.Errors); + } + detectionAccuracy = accuracyResult.Value; + } + + var command = new SearchTransfersCommand(budget, fromDate, tillDate, detectionAccuracy); + var transfersList = await mediator.Send(command, ct); + + // Return both recorded and unregistered transfers + var response = new TransfersListResponse( + transfersList.Recorded.Select(mapper.ToResponse).ToList(), + transfersList.Unregistered.Select(mapper.ToResponse).ToList() + ); + return Ok(response); + } + + /// + /// Registers new transfers in a specific budget + /// + /// Budget ID from route + /// Register transfers request + /// Cancellation token + /// Success or error details + [HttpPost] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task RegisterTransfers( + [FromRoute] Guid budgetId, + [FromBody] RegisterTransfersRequest request, + CancellationToken ct = default) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + // Collect all operation IDs needed + var operationIds = new HashSet(); + foreach (var transferRequest in request.Transfers) + { + operationIds.Add(transferRequest.SourceId); + operationIds.Add(transferRequest.SinkId); + } + + // Get all operations by IDs + var operationQuery = new OperationQuery( + o => operationIds.Contains(o.Id) + ); + var listQuery = new ListOperationsQuery(operationQuery); + + var operations = await mediator.CreateStream(listQuery, ct) + .ToDictionaryAsync(o => o.Id, ct); + + // Validate all operations exist + var missingIds = operationIds.Except(operations.Keys).ToList(); + if (missingIds.Any()) + { + return BadRequest(new List { new($"Operations not found: {string.Join(", ", missingIds)}") }); + } + + // Validate operations belong to the budget and build transfers + var transfers = new List(); + foreach (var transferRequest in request.Transfers) + { + var source = operations[transferRequest.SourceId]; + var sink = operations[transferRequest.SinkId]; + + // Validate operations belong to the same budget + if (source.Budget.Id != budgetId || sink.Budget.Id != budgetId) + { + return BadRequest(new List { new($"Operations must belong to budget {budgetId}") }); + } + + var transferResult = mapper.FromRequest(transferRequest, source, sink); + if (transferResult.IsFailed) + { + return BadRequest(transferResult.Errors); + } + + transfers.Add(transferResult.Value); + } + + // Build async enumerable + async IAsyncEnumerable GetTransfersAsync() + { + foreach (var transfer in transfers) + { + yield return transfer; + } + await Task.CompletedTask; + } + + var command = new RegisterTransfersCommand(GetTransfersAsync()); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } + + /// + /// Removes transfers from a specific budget + /// + /// Budget ID from route + /// Remove transfers request + /// Cancellation token + /// Success or error details + [HttpDelete] + [Consumes("application/json", "application/yaml", "text/yaml")] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(IEnumerable), 400)] + [ProducesResponseType(typeof(IEnumerable), 404)] + public async Task RemoveTransfers( + [FromRoute] Guid budgetId, + [FromBody] RemoveTransfersRequest request, + CancellationToken ct = default) + { + // Validate budget access + var budgets = await mediator.Send(new ListOwnedBudgetsQuery(), ct); + var budget = budgets.FirstOrDefault(b => b.Id == budgetId); + + if (budget == null) + { + return NotFound(new List { new($"Budget with ID {budgetId} not found or access denied") }); + } + + var command = new RemoveTransfersCommand(request.SourceIds.ToArray(), request.All); + var result = await mediator.Send(command, ct); + + if (result.IsSuccess) + { + return NoContent(); + } + + return BadRequest(result.Errors); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Exceptions/HttpException.cs b/src/Controllers/NVs.Budget.Controllers.Web/Exceptions/HttpException.cs new file mode 100644 index 00000000..d9ef6f7b --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Exceptions/HttpException.cs @@ -0,0 +1,77 @@ +using FluentResults; + +namespace NVs.Budget.Controllers.Web.Exceptions; + +public abstract class HttpException : Exception +{ + public int StatusCode { get; } + public IEnumerable Errors { get; } + + protected HttpException(int statusCode, string message) : base(message) + { + StatusCode = statusCode; + Errors = new List { new Error(message) }; + } + + protected HttpException(int statusCode, IEnumerable errors) : base(string.Join(", ", errors.Select(e => e.Message))) + { + StatusCode = statusCode; + Errors = errors; + } +} + +public class BadRequestException : HttpException +{ + public BadRequestException(string message) : base(400, message) + { + } + + public BadRequestException(IEnumerable errors) : base(400, errors) + { + } +} + +public class NotFoundException : HttpException +{ + public NotFoundException(string message) : base(404, message) + { + } + + public NotFoundException(IEnumerable errors) : base(404, errors) + { + } +} + +public class UnauthorizedException : HttpException +{ + public UnauthorizedException(string message) : base(401, message) + { + } + + public UnauthorizedException(IEnumerable errors) : base(401, errors) + { + } +} + +public class ForbiddenException : HttpException +{ + public ForbiddenException(string message) : base(403, message) + { + } + + public ForbiddenException(IEnumerable errors) : base(403, errors) + { + } +} + +public class ConflictException : HttpException +{ + public ConflictException(string message) : base(409, message) + { + } + + public ConflictException(IEnumerable errors) : base(409, errors) + { + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Filters/EnumSchemaFilter.cs b/src/Controllers/NVs.Budget.Controllers.Web/Filters/EnumSchemaFilter.cs new file mode 100644 index 00000000..9e8c85b5 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Filters/EnumSchemaFilter.cs @@ -0,0 +1,42 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace NVs.Budget.Controllers.Web.Filters; + +internal class EnumSchemaFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type.IsEnum) + { + schema.Enum.Clear(); + + // Get the enum values as strings + var enumValues = Enum.GetNames(context.Type); + foreach (var enumValue in enumValues) + { + schema.Enum.Add(new Microsoft.OpenApi.Any.OpenApiString(enumValue)); + } + + // Set the type to string since we're using string values + schema.Type = "string"; + } + else if (context.Type.IsGenericType) + { + // Handle nullable enums + var underlyingType = Nullable.GetUnderlyingType(context.Type); + if (underlyingType?.IsEnum == true) + { + schema.Enum.Clear(); + + var enumValues = Enum.GetNames(underlyingType); + foreach (var enumValue in enumValues) + { + schema.Enum.Add(new Microsoft.OpenApi.Any.OpenApiString(enumValue)); + } + + schema.Type = "string"; + } + } + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Filters/LowercaseDocumentFilter.cs b/src/Controllers/NVs.Budget.Controllers.Web/Filters/LowercaseDocumentFilter.cs new file mode 100644 index 00000000..220d8f80 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Filters/LowercaseDocumentFilter.cs @@ -0,0 +1,31 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace NVs.Budget.Controllers.Web.Filters; + +internal class LowercaseDocumentFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var paths = swaggerDoc.Paths.ToDictionary( + entry => ToLowercasePath(entry.Key), + entry => entry.Value + ); + + swaggerDoc.Paths.Clear(); + foreach (var (key, value) in paths) + { + swaggerDoc.Paths.Add(key, value); + } + } + + private static string ToLowercasePath(string path) + { + // Convert controller names to lowercase while preserving the rest of the path + return System.Text.RegularExpressions.Regex.Replace( + path, + @"/([A-Z][a-z]*)(?=/|$)", + match => "/" + match.Groups[1].Value.ToLowerInvariant() + ); + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Filters/ValidateModelStateFilter.cs b/src/Controllers/NVs.Budget.Controllers.Web/Filters/ValidateModelStateFilter.cs new file mode 100644 index 00000000..fdfd75d8 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Filters/ValidateModelStateFilter.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace NVs.Budget.Controllers.Web.Filters; + +/// +/// Action filter that validates model state and returns 400 Bad Request with errors +/// if the model state is invalid (e.g., from input formatter failures) +/// +internal class ValidateModelStateFilter : IActionFilter +{ + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + var errors = context.ModelState + .Where(x => x.Value?.Errors.Count > 0) + .SelectMany(x => x.Value!.Errors.Select(e => new + { + Field = x.Key, + Message = string.IsNullOrEmpty(e.ErrorMessage) ? e.Exception?.Message : e.ErrorMessage + })) + .ToList(); + + context.Result = new BadRequestObjectResult(errors); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + // No action needed after execution + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlInputFormatter.cs b/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlInputFormatter.cs new file mode 100644 index 00000000..83ddfa89 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlInputFormatter.cs @@ -0,0 +1,59 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc.Formatters; +using YamlDotNet.Serialization; + +namespace NVs.Budget.Controllers.Web.Formatters; + +internal class YamlInputFormatter : TextInputFormatter +{ + private readonly IDeserializer _deserializer; + + public YamlInputFormatter(IDeserializer deserializer) + { + _deserializer = deserializer; + + SupportedEncodings.Add(Encoding.UTF8); + SupportedEncodings.Add(Encoding.Unicode); + SupportedMediaTypes.Add("application/yaml"); + SupportedMediaTypes.Add("text/yaml"); + } + + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + { + var httpContext = context.HttpContext; + + using var reader = new StreamReader(httpContext.Request.Body, encoding); + try + { + var yaml = await reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(yaml)) + { + context.ModelState.AddModelError(string.Empty, "YAML content is empty"); + return await InputFormatterResult.FailureAsync(); + } + + var model = _deserializer.Deserialize(yaml, context.ModelType); + + if (model == null) + { + context.ModelState.AddModelError(string.Empty, "Failed to deserialize YAML to expected type"); + return await InputFormatterResult.FailureAsync(); + } + + return await InputFormatterResult.SuccessAsync(model); + } + catch (Exception ex) + { + var errorMessage = $"Failed to parse YAML: {ex.Message}"; + if (ex.InnerException != null) + { + errorMessage += $" Inner exception: {ex.InnerException.Message}"; + } + + context.ModelState.AddModelError(string.Empty, errorMessage); + return await InputFormatterResult.FailureAsync(); + } + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlOutputFormatter.cs b/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlOutputFormatter.cs new file mode 100644 index 00000000..1ba6a49e --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Formatters/YamlOutputFormatter.cs @@ -0,0 +1,30 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc.Formatters; +using YamlDotNet.Serialization; + +namespace NVs.Budget.Controllers.Web.Formatters; + +internal class YamlOutputFormatter : TextOutputFormatter +{ + private readonly ISerializer _serializer; + + public YamlOutputFormatter(ISerializer serializer) + { + _serializer = serializer; + + SupportedEncodings.Add(Encoding.UTF8); + SupportedEncodings.Add(Encoding.Unicode); + SupportedMediaTypes.Add("application/yaml"); + SupportedMediaTypes.Add("text/yaml"); + } + + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + var response = context.HttpContext.Response; + using (var writer = context.WriterFactory(response.Body, selectedEncoding)) + { + _serializer.Serialize(writer, context.Object); + await writer.FlushAsync(); + } + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Middleware/ExceptionHandlingMiddleware.cs b/src/Controllers/NVs.Budget.Controllers.Web/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 00000000..4ec4ce56 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using NVs.Budget.Controllers.Web.Exceptions; + +namespace NVs.Budget.Controllers.Web.Middleware; + +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (HttpException httpEx) + { + _logger.LogWarning(httpEx, "HTTP exception occurred: {Message}", httpEx.Message); + await HandleHttpExceptionAsync(context, httpEx); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception occurred: {Message}", ex.Message); + await HandleUnhandledExceptionAsync(context, ex); + } + } + + private static async Task HandleHttpExceptionAsync(HttpContext context, HttpException exception) + { + context.Response.StatusCode = exception.StatusCode; + context.Response.ContentType = "application/json"; + + var errors = exception.Errors.Select(e => new ErrorResponse(e.Message, e.Metadata)).ToList(); + var json = JsonSerializer.Serialize(errors, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + await context.Response.WriteAsync(json); + } + + private static async Task HandleUnhandledExceptionAsync(HttpContext context, Exception exception) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + + var errors = new List + { + new("An internal server error occurred. Please try again later.", new Dictionary()) + }; + + var json = JsonSerializer.Serialize(errors, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + await context.Response.WriteAsync(json); + } +} + +public record ErrorResponse(string Message, Dictionary Metadata); + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Models/BudgetResponse.cs b/src/Controllers/NVs.Budget.Controllers.Web/Models/BudgetResponse.cs new file mode 100644 index 00000000..d4b32183 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Models/BudgetResponse.cs @@ -0,0 +1,83 @@ +using NVs.Budget.Domain.Entities.Budgets; + +namespace NVs.Budget.Controllers.Web.Models; + +public record BudgetResponse( + Guid Id, + string Name, + string Version, + IReadOnlyCollection Owners, + IReadOnlyCollection TaggingCriteria, + IReadOnlyCollection TransferCriteria, + LogbookCriteriaResponse LogbookCriteria +); + +public class TaggingCriterionResponse +{ + public string Tag { get; set; } = string.Empty; + public string Condition { get; set; } = string.Empty; + + // Constructor for backward compatibility with tests + public TaggingCriterionResponse() { } + public TaggingCriterionResponse(string tag, string condition) + { + Tag = tag; + Condition = condition; + } +} + +public class TransferCriterionResponse +{ + public string Accuracy { get; set; } = string.Empty; + public string Comment { get; set; } = string.Empty; + public string Criterion { get; set; } = string.Empty; + + // Constructor for backward compatibility with tests + public TransferCriterionResponse() { } + public TransferCriterionResponse(string accuracy, string comment, string criterion) + { + Accuracy = accuracy; + Comment = comment; + Criterion = criterion; + } +} + +public class LogbookCriteriaResponse +{ + public string Description { get; set; } = string.Empty; + public List? Subcriteria { get; set; } + public string? Type { get; set; } + public List? Tags { get; set; } + public string? Substitution { get; set; } + public string? Criteria { get; set; } + public bool? IsUniversal { get; set; } + + // Constructor for backward compatibility with tests + public LogbookCriteriaResponse() { } + public LogbookCriteriaResponse( + string description, + IReadOnlyCollection? subcriteria, + string? type, + IReadOnlyCollection? tags, + string? substitution, + string? criteria, + bool? isUniversal) + { + Description = description; + Subcriteria = subcriteria?.ToList(); + Type = type; + Tags = tags?.ToList(); + Substitution = substitution; + Criteria = criteria; + IsUniversal = isUniversal; + } +} + +public class UpdateBudgetRequest +{ + public string Name { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public List? TaggingCriteria { get; set; } + public List? TransferCriteria { get; set; } + public LogbookCriteriaResponse? LogbookCriteria { get; set; } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Models/FileReadingSettingModels.cs b/src/Controllers/NVs.Budget.Controllers.Web/Models/FileReadingSettingModels.cs new file mode 100644 index 00000000..e1d534e7 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Models/FileReadingSettingModels.cs @@ -0,0 +1,20 @@ +namespace NVs.Budget.Controllers.Web.Models; + +public class ValidationRuleResponse +{ + public string Pattern { get; set; } = string.Empty; + public string Condition { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string ErrorMessage { get; set; } = string.Empty; +} + +public class FileReadingSettingResponse +{ + public string Culture { get; set; } = string.Empty; + public string Encoding { get; set; } = string.Empty; + public string DateTimeKind { get; set; } = string.Empty; + public Dictionary Fields { get; set; } = new(); + public Dictionary Attributes { get; set; } = new(); + public List Validation { get; set; } = new(); +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Models/OperationResponse.cs b/src/Controllers/NVs.Budget.Controllers.Web/Models/OperationResponse.cs new file mode 100644 index 00000000..c20db113 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Models/OperationResponse.cs @@ -0,0 +1,105 @@ +using FluentResults; + +namespace NVs.Budget.Controllers.Web.Models; + +public record OperationResponse( + Guid Id, + string Version, + DateTime Timestamp, + MoneyResponse Amount, + string Description, + Guid BudgetId, + IReadOnlyCollection Tags, + Dictionary? Attributes +); + +public record MoneyResponse( + decimal Value, + string CurrencyCode +); + +public record UnregisteredOperationRequest( + DateTime Timestamp, + MoneyResponse Amount, + string Description, + Dictionary? Attributes +); + +public record UpdateOperationRequest( + Guid Id, + string Version, + DateTime Timestamp, + MoneyResponse Amount, + string Description, + IReadOnlyCollection Tags, + Dictionary? Attributes +); + +public record UpdateOperationsRequest( + string BudgetVersion, + IReadOnlyCollection Operations, + string? TransferConfidenceLevel, + string TaggingMode +); + +public record RemoveOperationsRequest( + string Criteria +); + +public record RetagOperationsRequest( + string BudgetVersion, + string Criteria, + bool FromScratch +); + +// Result response models +public record ImportResultResponse( + IReadOnlyCollection RegisteredOperations, + IReadOnlyCollection> Duplicates, + IReadOnlyCollection Errors, + IReadOnlyCollection Successes); + +public record UpdateResultResponse( + IReadOnlyCollection UpdatedOperations, + IReadOnlyCollection Errors, + IReadOnlyCollection Successes +); + +public record DeleteResultResponse( + IReadOnlyCollection Errors, + IReadOnlyCollection Successes +); + +public record RetagResultResponse( + IReadOnlyCollection Errors, + IReadOnlyCollection Successes +); + +// Logbook models +public record NamedRangeResponse( + string Name, + DateTime From, + DateTime Till +); + +public record LogbookEntryResponse( + string Description, + MoneyResponse Sum, + DateTime From, + DateTime Till, + int OperationsCount, + IReadOnlyCollection Operations, + IReadOnlyCollection Children +); + +public record RangedLogbookEntryResponse( + NamedRangeResponse Range, + LogbookEntryResponse Entry +); + +public record LogbookResponse( + IReadOnlyCollection Ranges, + IReadOnlyCollection Errors, + IReadOnlyCollection Successes +); + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Models/TransferResponse.cs b/src/Controllers/NVs.Budget.Controllers.Web/Models/TransferResponse.cs new file mode 100644 index 00000000..dae387ea --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Models/TransferResponse.cs @@ -0,0 +1,36 @@ +using FluentResults; + +namespace NVs.Budget.Controllers.Web.Models; + +public record TransferResponse( + Guid SourceId, + OperationResponse Source, + Guid SinkId, + OperationResponse Sink, + MoneyResponse Fee, + string Comment, + string Accuracy +); + +public record TransfersListResponse( + IReadOnlyCollection Recorded, + IReadOnlyCollection Unregistered +); + +public record RegisterTransferRequest( + Guid SourceId, + Guid SinkId, + MoneyResponse? Fee, + string Comment, + string Accuracy +); + +public record RegisterTransfersRequest( + IReadOnlyCollection Transfers +); + +public record RemoveTransfersRequest( + IReadOnlyCollection SourceIds, + bool All +); + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/NVs.Budget.Controllers.Web.csproj b/src/Controllers/NVs.Budget.Controllers.Web/NVs.Budget.Controllers.Web.csproj new file mode 100644 index 00000000..cc85de26 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/NVs.Budget.Controllers.Web.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/BudgetMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/BudgetMapper.cs new file mode 100644 index 00000000..5d0ada87 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/BudgetMapper.cs @@ -0,0 +1,161 @@ +using FluentResults; +using NVs.Budget.Application.Contracts.Criteria; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Domain.Entities.Operations; +using NVs.Budget.Utilities.Expressions; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class BudgetMapper(ReadableExpressionsParser parser) +{ + public BudgetResponse ToResponse(TrackedBudget budget) + { + return new BudgetResponse( + budget.Id, + budget.Name, + budget.Version ?? string.Empty, + budget.Owners, + budget.TaggingCriteria.Select(ToResponse).ToList(), + budget.TransferCriteria.Select(ToResponse).ToList(), + ToResponse(budget.LogbookCriteria) + ); + } + + private TaggingCriterionResponse ToResponse(TaggingCriterion criterion) + { + return new TaggingCriterionResponse + { + Tag = criterion.Tag.ToString(), + Condition = criterion.Condition.ToString() + }; + } + + private TransferCriterionResponse ToResponse(TransferCriterion criterion) + { + return new TransferCriterionResponse + { + Accuracy = criterion.Accuracy.ToString(), + Comment = criterion.Comment, + Criterion = criterion.Criterion.ToString() + }; + } + + private LogbookCriteriaResponse ToResponse(LogbookCriteria criteria) + { + return new LogbookCriteriaResponse + { + Description = criteria.Description, + Subcriteria = criteria.Subcriteria?.Select(ToResponse).ToList(), + Type = criteria.Type?.ToString(), + Tags = criteria.Tags?.Select(t => t.Value).ToList(), + Substitution = criteria.Substitution?.ToString(), + Criteria = criteria.Criteria?.ToString(), + IsUniversal = criteria.IsUniversal + }; + } + + public Result FromRequest(TaggingCriterionResponse request) + { + var tagResult = parser.ParseUnaryConversion(request.Tag); + if (tagResult.IsFailed) + { + return Result.Fail(tagResult.Errors); + } + + var conditionResult = parser.ParseUnaryPredicate(request.Condition); + if (conditionResult.IsFailed) + { + return Result.Fail(conditionResult.Errors); + } + + return Result.Ok(new TaggingCriterion(tagResult.Value, conditionResult.Value)); + } + + public Result FromRequest(TransferCriterionResponse request) + { + if (!Enum.TryParse(request.Accuracy, out var accuracy)) + { + return Result.Fail($"Invalid DetectionAccuracy value: {request.Accuracy}"); + } + + var criterionResult = parser.ParseBinaryPredicate(request.Criterion); + if (criterionResult.IsFailed) + { + return Result.Fail(criterionResult.Errors); + } + + return Result.Ok(new TransferCriterion(accuracy, request.Comment, criterionResult.Value)); + } + + public Result FromRequest(LogbookCriteriaResponse request) + { + // Parse subcriteria recursively if present + List? subcriteria = null; + if (request.Subcriteria != null) + { + subcriteria = new List(); + foreach (var sub in request.Subcriteria) + { + var subResult = FromRequest(sub); + if (subResult.IsFailed) + { + return Result.Fail(subResult.Errors); + } + subcriteria.Add(subResult.Value); + } + } + + // Parse Type if present + Domain.ValueObjects.Criteria.TagBasedCriterionType? type = null; + if (request.Type != null) + { + if (!Enum.TryParse(request.Type, out var parsedType)) + { + return Result.Fail($"Invalid TagBasedCriterionType value: {request.Type}"); + } + type = parsedType; + } + + // Parse Tags if present + Domain.ValueObjects.Tag[]? tags = null; + if (request.Tags != null) + { + tags = request.Tags.Select(t => new Domain.ValueObjects.Tag(t)).ToArray(); + } + + // Parse Substitution if present + ReadableExpression>? substitution = null; + if (request.Substitution != null) + { + var substitutionResult = parser.ParseUnaryConversion(request.Substitution); + if (substitutionResult.IsFailed) + { + return Result.Fail(substitutionResult.Errors); + } + substitution = substitutionResult.Value; + } + + // Parse Criteria if present + ReadableExpression>? criteria = null; + if (request.Criteria != null) + { + var criteriaResult = parser.ParseUnaryPredicate(request.Criteria); + if (criteriaResult.IsFailed) + { + return Result.Fail(criteriaResult.Errors); + } + criteria = criteriaResult.Value; + } + + return Result.Ok(new LogbookCriteria( + request.Description, + subcriteria, + type, + tags, + substitution, + criteria, + request.IsUniversal + )); + } +} diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/FileReadingSettingsMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/FileReadingSettingsMapper.cs new file mode 100644 index 00000000..1d2b48e9 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/FileReadingSettingsMapper.cs @@ -0,0 +1,138 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using FluentResults; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class FileReadingSettingsMapper +{ + public Dictionary ToResponse(IReadOnlyDictionary settings) + { + var responseSettings = new Dictionary(); + + foreach (var kvp in settings) + { + var pattern = kvp.Key.ToString(); + var setting = kvp.Value; + + var validationRules = setting.Validation + .Select(v => new ValidationRuleResponse + { + Pattern = v.Pattern, + Condition = v.Condition.ToString(), + Value = v.Value, + ErrorMessage = v.ErrorMessage + }) + .ToList(); + + var response = new FileReadingSettingResponse + { + Culture = setting.Culture.Name, + Encoding = setting.Encoding.WebName, + DateTimeKind = setting.DateTimeKind.ToString(), + Fields = new Dictionary(setting.Fields), + Attributes = new Dictionary(setting.Attributes), + Validation = validationRules + }; + + responseSettings[pattern] = response; + } + + return responseSettings; + } + + public Result> FromRequest(Dictionary request) + { + var settings = new Dictionary(); + var errors = new List(); + + foreach (var kvp in request) + { + var patternString = kvp.Key; + var settingResponse = kvp.Value; + + // Parse regex pattern + Regex regex; + try + { + regex = new Regex(patternString); + } + catch (ArgumentException ex) + { + errors.Add(new Error($"Invalid regex pattern '{patternString}': {ex.Message}")); + continue; + } + + // Parse culture + CultureInfo culture; + try + { + culture = CultureInfo.GetCultureInfo(settingResponse.Culture); + } + catch (CultureNotFoundException ex) + { + errors.Add(new Error($"Invalid culture '{settingResponse.Culture}': {ex.Message}")); + continue; + } + + // Parse encoding + Encoding encoding; + try + { + encoding = Encoding.GetEncoding(settingResponse.Encoding); + } + catch (ArgumentException ex) + { + errors.Add(new Error($"Invalid encoding '{settingResponse.Encoding}': {ex.Message}")); + continue; + } + + // Parse DateTimeKind + if (!Enum.TryParse(settingResponse.DateTimeKind, out var dateTimeKind)) + { + errors.Add(new Error($"Invalid DateTimeKind '{settingResponse.DateTimeKind}'. Must be one of: Local, Utc, Unspecified")); + continue; + } + + // Parse validation rules + var validationRules = new List(); + foreach (var vrResponse in settingResponse.Validation) + { + if (!Enum.TryParse(vrResponse.Condition, out var condition)) + { + errors.Add(new Error($"Invalid ValidationCondition '{vrResponse.Condition}'. Must be one of: Equals, NotEquals")); + continue; + } + + validationRules.Add(new ValidationRule( + vrResponse.Pattern, + condition, + vrResponse.Value, + vrResponse.ErrorMessage + )); + } + + var setting = new FileReadingSetting( + culture, + encoding, + dateTimeKind, + settingResponse.Fields, + settingResponse.Attributes, + validationRules + ); + + settings[regex] = setting; + } + + if (errors.Any()) + { + return Result.Fail(errors); + } + + return Result.Ok>(settings); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/LogbookMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/LogbookMapper.cs new file mode 100644 index 00000000..38e2d2a2 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/LogbookMapper.cs @@ -0,0 +1,36 @@ +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Domain.Aggregates; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class LogbookMapper(OperationMapper operationMapper) +{ + public LogbookEntryResponse ToResponse(CriteriaBasedLogbook logbook) + { + var sum = logbook.Sum; + var currency = sum.GetCurrency(); + var children = logbook.Children + .Select(kvp => ToResponse(kvp.Value)) + .ToList(); + + // Only include operations if there are no children (leaf node) + var operations = children.Count == 0 + ? logbook.Operations + .Cast() + .Select(operationMapper.ToResponse) + .ToList() + : new List(); + + return new LogbookEntryResponse( + logbook.Criterion.Description, + new MoneyResponse(sum.Amount, currency.IsoCode.ToString()), + logbook.From, + logbook.Till, + logbook.Operations.Count(), + operations, + children + ); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/MoneyMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/MoneyMapper.cs new file mode 100644 index 00000000..68af5336 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/MoneyMapper.cs @@ -0,0 +1,23 @@ +using FluentResults; +using NMoneys; +using NVs.Budget.Controllers.Web.Models; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class MoneyMapper +{ + public Result ParseMoney(MoneyResponse moneyResponse) + { + try + { + var currency = Currency.Get(moneyResponse.CurrencyCode); + var money = new Money(moneyResponse.Value, currency); + return Result.Ok(money); + } + catch (Exception ex) + { + return Result.Fail($"Invalid money value: {ex.Message}"); + } + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationExtensions.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationExtensions.cs new file mode 100644 index 00000000..56f620e8 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationExtensions.cs @@ -0,0 +1,33 @@ +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; + +namespace NVs.Budget.Controllers.Web.Utils; + +public static class OperationExtensions +{ + /// + /// Orders operations by timestamp in descending order (newest first) + /// + public static IOrderedEnumerable OrderHistorically(this IEnumerable operations) + { + return operations.OrderByDescending(o => o.Timestamp); + } + + /// + /// Orders operations by timestamp in descending order (newest first) + /// + public static IOrderedAsyncEnumerable OrderHistorically(this IAsyncEnumerable operations) + { + return operations.OrderByDescending(o => o.Timestamp); + } + + /// + /// Orders groups of operations by the timestamp of the first operation in each group (newest first) + /// + public static IOrderedEnumerable> OrderHistorically( + this IEnumerable> groups) + { + return groups.OrderByDescending(group => group.FirstOrDefault()?.Timestamp ?? DateTime.MinValue); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationMapper.cs new file mode 100644 index 00000000..27a6951b --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/OperationMapper.cs @@ -0,0 +1,111 @@ +using FluentResults; +using NMoneys; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Domain.Entities.Operations; +using NVs.Budget.Domain.ValueObjects; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class OperationMapper(MoneyMapper moneyMapper) +{ + public OperationResponse ToResponse(TrackedOperation operation) + { + return new OperationResponse( + operation.Id, + operation.Version ?? string.Empty, + operation.Timestamp, + new MoneyResponse(operation.Amount.Amount, operation.Amount.CurrencyCode.ToString()), + operation.Description, + operation.Budget.Id, + operation.Tags.Select(t => t.Value).ToList(), + operation.Attributes.Count > 0 ? new Dictionary(operation.Attributes) : null + ); + } + + public OperationResponse ToResponse(Operation operation) + { + // If it's a TrackedOperation, use the TrackedOperation overload + if (operation is TrackedOperation trackedOperation) + { + return ToResponse(trackedOperation); + } + + // Otherwise, map as base Operation + return new OperationResponse( + operation.Id, + string.Empty, // Operation doesn't have Version + operation.Timestamp, + new MoneyResponse(operation.Amount.Amount, operation.Amount.CurrencyCode.ToString()), + operation.Description, + operation.Budget.Id, + operation.Tags.Select(t => t.Value).ToList(), + operation.Attributes.Count > 0 ? new Dictionary(operation.Attributes) : null + ); + } + + public Result FromRequest(UnregisteredOperationRequest request) + { + var moneyResult = moneyMapper.ParseMoney(request.Amount); + if (moneyResult.IsFailed) + { + return Result.Fail(moneyResult.Errors); + } + + var attributes = request.Attributes != null + ? new Dictionary(request.Attributes) + : null; + + return Result.Ok(new UnregisteredOperation( + request.Timestamp, + moneyResult.Value, + request.Description, + attributes + )); + } + + public Result FromRequest(UpdateOperationRequest request, TrackedBudget budget) + { + var moneyResult = moneyMapper.ParseMoney(request.Amount); + if (moneyResult.IsFailed) + { + return Result.Fail(moneyResult.Errors); + } + + var tags = request.Tags.Select(t => new Tag(t)).ToList(); + var attributes = request.Attributes != null + ? new Dictionary(request.Attributes) + : null; + + var operation = new TrackedOperation( + request.Id, + request.Timestamp, + moneyResult.Value, + request.Description, + budget, + tags, + attributes + ) + { + Version = request.Version + }; + + return Result.Ok(operation); + } + + public Result ParseDetectionAccuracy(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return Result.Ok(default); + } + + if (!Enum.TryParse(value, true, out var accuracy)) + { + return Result.Fail($"Invalid DetectionAccuracy value: {value}"); + } + + return Result.Ok(accuracy); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/RangeBuilder.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/RangeBuilder.cs new file mode 100644 index 00000000..8bfb9ed5 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/RangeBuilder.cs @@ -0,0 +1,60 @@ +using FluentResults; +using NCrontab; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class RangeBuilder +{ + public Result> GetRanges(DateTime from, DateTime till, string? cronExpr) + { + if (till < from) + { + return Result.Fail("Till date must be after From date"); + } + + // If no cron expression, return single range + if (string.IsNullOrWhiteSpace(cronExpr)) + { + var name = $"{from:dd/MM} - {till:dd/MM}"; + return Result.Ok>(new[] { new NamedRange(name, from, till) }); + } + + CrontabSchedule schedule; + try + { + schedule = CrontabSchedule.Parse(cronExpr); + } + catch (Exception e) + { + return Result.Fail($"Invalid cron expression: {e.Message}"); + } + + var occurrences = schedule.GetNextOccurrences(from.AddDays(-1), till.AddDays(1)) + .OrderBy(d => d) + .ToList(); + + if (occurrences.Count < 2) + { + return Result.Fail("Cron expression must generate at least 2 occurrences within the date range"); + } + + return Result.Ok(GenerateRangesFrom(occurrences)); + } + + private IEnumerable GenerateRangesFrom(List occurrences) + { + var i = 1; + while (i < occurrences.Count) + { + yield return new NamedRange( + occurrences[i - 1].ToString("dd/MM"), + occurrences[i - 1], + occurrences[i] + ); + i++; + } + } +} + +public record NamedRange(string Name, DateTime From, DateTime Till); + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/Utils/TransferMapper.cs b/src/Controllers/NVs.Budget.Controllers.Web/Utils/TransferMapper.cs new file mode 100644 index 00000000..ec1e3630 --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/Utils/TransferMapper.cs @@ -0,0 +1,102 @@ +using FluentResults; +using NMoneys; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Controllers.Web.Models; +using NVs.Budget.Domain.Entities.Transactions; + +namespace NVs.Budget.Controllers.Web.Utils; + +public class TransferMapper(OperationMapper operationMapper, MoneyMapper moneyMapper) +{ + public TransferResponse ToResponse(TrackedTransfer transfer) + { + return new TransferResponse( + transfer.Source.Id, + operationMapper.ToResponse(transfer.Source), // Will check if it's TrackedOperation internally + transfer.Sink.Id, + operationMapper.ToResponse(transfer.Sink), // Will check if it's TrackedOperation internally + new MoneyResponse(transfer.Fee.Amount, transfer.Fee.CurrencyCode.ToString()), + transfer.Comment, + transfer.Accuracy.ToString() + ); + } + + public TransferResponse ToResponse(Transfer transfer, string accuracy = "Unknown") + { + // If it's a TrackedTransfer, use the TrackedTransfer overload + if (transfer is TrackedTransfer trackedTransfer) + { + return ToResponse(trackedTransfer); + } + + // Otherwise, map as base Transfer + return new TransferResponse( + transfer.Source.Id, + operationMapper.ToResponse(transfer.Source), + transfer.Sink.Id, + operationMapper.ToResponse(transfer.Sink), + new MoneyResponse(transfer.Fee.Amount, transfer.Fee.CurrencyCode.ToString()), + transfer.Comment, + accuracy + ); + } + + public Result FromRequest( + RegisterTransferRequest request, + TrackedOperation source, + TrackedOperation sink) + { + // Parse fee if provided + Money fee; + if (request.Fee != null) + { + var feeResult = moneyMapper.ParseMoney(request.Fee); + if (feeResult.IsFailed) + { + return Result.Fail(feeResult.Errors); + } + fee = feeResult.Value; + } + else + { + // Calculate fee as difference + fee = sink.Amount + source.Amount; + } + + // Parse accuracy + var accuracyResult = operationMapper.ParseDetectionAccuracy(request.Accuracy); + if (accuracyResult.IsFailed) + { + return Result.Fail(accuracyResult.Errors); + } + + var transfer = new UnregisteredTransfer( + source, + sink, + fee, + request.Comment, + accuracyResult.Value + ); + + return Result.Ok(transfer); + } + + public TransferResponse ToResponse(UnregisteredTransfer transfer) + { + return new TransferResponse( + transfer.Source.Id, + operationMapper.ToResponse(transfer.Source), + transfer.Sink.Id, + operationMapper.ToResponse(transfer.Sink), + new MoneyResponse(transfer.Fee.Amount, transfer.Fee.CurrencyCode.ToString()), + transfer.Comment, + transfer.Accuracy.ToString() + ); + } + + public Result ParseDetectionAccuracy(string? value) + { + return operationMapper.ParseDetectionAccuracy(value); + } +} + diff --git a/src/Controllers/NVs.Budget.Controllers.Web/WebControllersExtensions.cs b/src/Controllers/NVs.Budget.Controllers.Web/WebControllersExtensions.cs new file mode 100644 index 00000000..d8b83a4d --- /dev/null +++ b/src/Controllers/NVs.Budget.Controllers.Web/WebControllersExtensions.cs @@ -0,0 +1,115 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Asp.Versioning; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using NVs.Budget.Controllers.Web.Filters; +using NVs.Budget.Controllers.Web.Formatters; +using NVs.Budget.Controllers.Web.Middleware; +using NVs.Budget.Controllers.Web.Utils; +using Swashbuckle.AspNetCore.SwaggerGen; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +[assembly: InternalsVisibleTo("NVs.Budget.Controllers.Web.Tests")] + +namespace NVs.Budget.Controllers.Web; + +public static class WebControllersExtensions +{ + public static IServiceCollection AddWebControllers(this IServiceCollection services) + { + services.AddSingleton(new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build()); + + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(o => + { + o.SwaggerDoc("v0.1", new OpenApiInfo { Title = "Budget API", Version = "v0.1" }); + o.DocInclusionPredicate((docName, apiDesc) => + { + if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) + return false; + var versions = methodInfo.DeclaringType! + .GetCustomAttributes(true) + .OfType() + .SelectMany(attr => attr.Versions); + var versionMatched = versions.Any(v => $"v{v}" == docName); + if (versionMatched) + { + if (apiDesc.RelativePath?.StartsWith("api/v{version}/") == true) + { + apiDesc.RelativePath = apiDesc.RelativePath.Replace("api/v{version}/", $"api/{docName}/"); + var versionParam = apiDesc.ParameterDescriptions + .SingleOrDefault(p => p.Name == "version" && p.Source.Id == "Path"); + if (versionParam != null) + apiDesc.ParameterDescriptions.Remove(versionParam); + } + } + + return versionMatched; + }); + + // Configure lowercase paths + o.DocumentFilter(); + + // Configure enum string values + o.UseInlineDefinitionsForEnums(); + o.SchemaFilter(); + }); + + var assembly = typeof(WebControllersExtensions).Assembly; + var part = new AssemblyPart(assembly); + services + .AddControllersWithViews(opts => + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + opts.OutputFormatters.Add(new YamlOutputFormatter(serializer)); + opts.InputFormatters.Insert(0, new YamlInputFormatter(deserializer)); + opts.FormatterMappings.SetMediaTypeMappingForFormat("yaml", "application/yaml"); + + // Add model state validation filter to return 400 on invalid input + opts.Filters.Add(); + }) + .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part)); + + services.AddApiVersioning(); + + + return services; + } + + public static WebApplication UseWebControllers(this WebApplication app, bool useSwagger) + { + // Add exception handling middleware first to catch all exceptions + app.UseMiddleware(); + + if (useSwagger) + { + app.UseSwagger(); + app.UseSwaggerUI(o => o.SwaggerEndpoint("/swagger/v0.1/swagger.json", "Budget API v0.1")); + } + + return app; + } +} diff --git a/src/Domain/NVs.Budget.Domain.Tests/CriteriaBasedLogbookShould.cs b/src/Domain/NVs.Budget.Domain.Tests/CriteriaBasedLogbookShould.cs index 0cd4e20e..ebeacc55 100644 --- a/src/Domain/NVs.Budget.Domain.Tests/CriteriaBasedLogbookShould.cs +++ b/src/Domain/NVs.Budget.Domain.Tests/CriteriaBasedLogbookShould.cs @@ -60,6 +60,19 @@ public void CreateSubRangedLogbooksWithSameCriteria() logBook.Children[futureCriterion].Operations.Should().BeEquivalentTo(expectedFutureTransactions); } + [Fact] + public void ReturnCriteriaBasedLogbookForEmptyRange() + { + var criterion = new UniversalCriterion("Anyone"); + var logbook = new CriteriaBasedLogbook(criterion); + + var ranged = logbook[DateTime.MinValue, DateTime.MaxValue]; + + ranged.Should().BeOfType(); + ((CriteriaBasedLogbook)ranged).Criterion.Should().Be(criterion); + ranged.IsEmpty.Should().BeTrue(); + } + [Fact] public void NotAddTransactionIfItDoesNotMatchCriterion() { diff --git a/src/Domain/NVs.Budget.Domain.Tests/NVs.Budget.Domain.Tests.csproj b/src/Domain/NVs.Budget.Domain.Tests/NVs.Budget.Domain.Tests.csproj index c63d45ea..ea5150ac 100644 --- a/src/Domain/NVs.Budget.Domain.Tests/NVs.Budget.Domain.Tests.csproj +++ b/src/Domain/NVs.Budget.Domain.Tests/NVs.Budget.Domain.Tests.csproj @@ -18,7 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Domain/NVs.Budget.Domain/Aggregates/CriteriaBasedLogbook.cs b/src/Domain/NVs.Budget.Domain/Aggregates/CriteriaBasedLogbook.cs index 753d541a..433a6cfc 100644 --- a/src/Domain/NVs.Budget.Domain/Aggregates/CriteriaBasedLogbook.cs +++ b/src/Domain/NVs.Budget.Domain/Aggregates/CriteriaBasedLogbook.cs @@ -25,7 +25,7 @@ public override Result Register(Operation o) { if (!Criterion.Matched(o)) return Result.Fail(new OperationDidNotMatchCriteriaError() - .WithTransactionId(o) + .WithOperationId(o) .WithMetadata(nameof(Criterion), Criterion) ); @@ -34,7 +34,7 @@ public override Result Register(Operation o) { var subcriterion = Criterion.GetMatchedSubcriterion(o); if (subcriterion is null) - return Result.Fail(new OperationDidNotMatchSubcriteriaError().WithTransactionId(o) + return Result.Fail(new OperationDidNotMatchSubcriteriaError().WithOperationId(o) .WithMetadata(nameof(Criterion), Criterion) ); @@ -50,4 +50,17 @@ public override Result Register(Operation o) } protected override Logbook CreateSubRangedLogbook() => new CriteriaBasedLogbook(Criterion); + + public override Logbook this[DateTime from, DateTime till] + { + get + { + if (IsEmpty) + { + return new CriteriaBasedLogbook(Criterion); + } + + return base[from, till]; + } + } } diff --git a/src/Domain/NVs.Budget.Domain/Entities/Accounts/Budget.cs b/src/Domain/NVs.Budget.Domain/Entities/Budgets/Budget.cs similarity index 95% rename from src/Domain/NVs.Budget.Domain/Entities/Accounts/Budget.cs rename to src/Domain/NVs.Budget.Domain/Entities/Budgets/Budget.cs index f2d4c489..fa50c2ae 100644 --- a/src/Domain/NVs.Budget.Domain/Entities/Accounts/Budget.cs +++ b/src/Domain/NVs.Budget.Domain/Entities/Budgets/Budget.cs @@ -1,4 +1,4 @@ -namespace NVs.Budget.Domain.Entities.Accounts; +namespace NVs.Budget.Domain.Entities.Budgets; public class Budget : EntityBase { diff --git a/src/Domain/NVs.Budget.Domain/Entities/Accounts/Owner.cs b/src/Domain/NVs.Budget.Domain/Entities/Budgets/Owner.cs similarity index 82% rename from src/Domain/NVs.Budget.Domain/Entities/Accounts/Owner.cs rename to src/Domain/NVs.Budget.Domain/Entities/Budgets/Owner.cs index c9bb3a54..fdb4d390 100644 --- a/src/Domain/NVs.Budget.Domain/Entities/Accounts/Owner.cs +++ b/src/Domain/NVs.Budget.Domain/Entities/Budgets/Owner.cs @@ -1,4 +1,4 @@ -namespace NVs.Budget.Domain.Entities.Accounts; +namespace NVs.Budget.Domain.Entities.Budgets; public class Owner : EntityBase { diff --git a/src/Domain/NVs.Budget.Domain/Entities/Operations/Operation.cs b/src/Domain/NVs.Budget.Domain/Entities/Operations/Operation.cs index fef6dabf..e76817ce 100644 --- a/src/Domain/NVs.Budget.Domain/Entities/Operations/Operation.cs +++ b/src/Domain/NVs.Budget.Domain/Entities/Operations/Operation.cs @@ -12,13 +12,13 @@ public class Operation : EntityBase public DateTime Timestamp { get; } public Money Amount { get; } public string Description { get; } - public Accounts.Budget Budget { get; } + public Budgets.Budget Budget { get; } public IReadOnlyCollection Tags => _tags.AsReadOnly(); public IDictionary Attributes { get; } = new AttributesDictionary(new Dictionary()); - public Operation(Guid id, DateTime timestamp, Money amount, string description, Accounts.Budget budget, IEnumerable tags, IReadOnlyDictionary? attributes) : base(id) + public Operation(Guid id, DateTime timestamp, Money amount, string description, Budgets.Budget budget, IEnumerable tags, IReadOnlyDictionary? attributes) : base(id) { Timestamp = timestamp; Amount = amount; @@ -46,5 +46,10 @@ public void Tag(Tag value) public void Untag(Tag value) => _tags.Remove(value); - public void UntagAll() => _tags.Clear(); + public void ResetTagsExcept(params Tag[] tags) + { + var preserved = _tags.Intersect(tags); + _tags.Clear(); + _tags.AddRange(preserved); + } } diff --git a/src/Domain/NVs.Budget.Domain/Entities/Transactions/Transfer.cs b/src/Domain/NVs.Budget.Domain/Entities/Transactions/Transfer.cs index 4829ce24..bc71923b 100644 --- a/src/Domain/NVs.Budget.Domain/Entities/Transactions/Transfer.cs +++ b/src/Domain/NVs.Budget.Domain/Entities/Transactions/Transfer.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; using NMoneys; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.Extensions; @@ -85,6 +86,10 @@ public Transfer(Operation source, Operation sink, Money fee, string comment) public Money Fee { get; } + public DateTime StartedAt => Source.Timestamp; + + public DateTime CompletedAt => Sink.Timestamp; + public Operation AsTransaction() { var timestamp = Source.Timestamp; diff --git a/src/Domain/NVs.Budget.Domain/Extensions/ExceptionExtensions.cs b/src/Domain/NVs.Budget.Domain/Extensions/ExceptionExtensions.cs index 4a9254d2..dc353f8f 100644 --- a/src/Domain/NVs.Budget.Domain/Extensions/ExceptionExtensions.cs +++ b/src/Domain/NVs.Budget.Domain/Extensions/ExceptionExtensions.cs @@ -8,9 +8,9 @@ public static class ExceptionExtensions public static Exception SetOperationId(this Exception e, Operation operation) => e.WithOperationId(operation); - public static Exception WithAccountId(this Exception e, Entities.Accounts.Budget budget) => WithData(e, $"{nameof(Entities.Accounts.Budget)}.{nameof(Operation.Id)}", budget.Id); + public static Exception WithBudgetId(this Exception e, Entities.Budgets.Budget budget) => WithData(e, $"{nameof(Entities.Budgets.Budget)}.{nameof(Operation.Id)}", budget.Id); - public static Exception SetAccountId(this Exception e, Entities.Accounts.Budget budget) => e.WithAccountId(budget); + public static Exception SetBudgetId(this Exception e, Entities.Budgets.Budget budget) => e.WithBudgetId(budget); public static Exception WithData(this Exception e, string key, object? value) { diff --git a/src/Domain/NVs.Budget.Domain/Extensions/ReasonExtensions.cs b/src/Domain/NVs.Budget.Domain/Extensions/ReasonExtensions.cs index d76aa64f..49eda565 100644 --- a/src/Domain/NVs.Budget.Domain/Extensions/ReasonExtensions.cs +++ b/src/Domain/NVs.Budget.Domain/Extensions/ReasonExtensions.cs @@ -11,14 +11,14 @@ public static T WithMetadata(this T reason, string key, object? value) where return reason; } - public static T WithTransactionId(this T reason, Operation t) where T: IReason + public static T WithOperationId(this T reason, Operation t) where T: IReason { return reason.WithMetadata($"{nameof(Operation)}.{nameof(Operation.Id)}", t.Id); } - public static T WithOperationId(this T reason, Entities.Accounts.Budget a) where T : IReason + public static T WithBudgetId(this T reason, Entities.Budgets.Budget a) where T : IReason { - return reason.WithMetadata($"{nameof(Entities.Accounts.Budget)}.{nameof(Entities.Accounts.Budget.Id)}", a.Id); + return reason.WithMetadata($"{nameof(Entities.Budgets.Budget)}.{nameof(Entities.Budgets.Budget.Id)}", a.Id); } public static Result WithReason(this Result result, IReason reason) diff --git a/src/Domain/NVs.Budget.Domain/ValueObjects/Criteria/SubstitutionBasedCriterion.cs b/src/Domain/NVs.Budget.Domain/ValueObjects/Criteria/SubstitutionBasedCriterion.cs index fbff084c..f18e3a09 100644 --- a/src/Domain/NVs.Budget.Domain/ValueObjects/Criteria/SubstitutionBasedCriterion.cs +++ b/src/Domain/NVs.Budget.Domain/ValueObjects/Criteria/SubstitutionBasedCriterion.cs @@ -2,35 +2,29 @@ namespace NVs.Budget.Domain.ValueObjects.Criteria; -public class SubstitutionBasedCriterion : Criterion +public class SubstitutionBasedCriterion(string description, Func substitution) : Criterion(description) { private static readonly UniversalCriterion Dummy = new(string.Empty); - private readonly List _subcriteria = [Dummy]; - private readonly Func _substitution; + private readonly SortedList _subcriteria = new() { { Dummy.Description, Dummy } }; - public SubstitutionBasedCriterion(string description, Func substitution) : base(description) - { - _substitution = substitution; - } - - public override IReadOnlyList Subcriteria => _subcriteria.AsReadOnly(); + public override IReadOnlyList Subcriteria => _subcriteria.Values.AsReadOnly(); public override bool Matched(Operation t) => true; - public override Criterion? GetMatchedSubcriterion(Operation t) + public override Criterion GetMatchedSubcriterion(Operation t) { var found = base.GetMatchedSubcriterion(t); if (found is not null && !ReferenceEquals(found, Dummy)) return found; - if (_subcriteria.All(c => ReferenceEquals(c, Dummy))) + if (_subcriteria.All(c => ReferenceEquals(c.Value, Dummy))) { _subcriteria.Clear(); } - var description = _substitution(t); - var criterion = new PredicateBasedCriterion(description, o => _substitution(o) == description); - _subcriteria.Add(criterion); + var description = substitution(t); + var criterion = new PredicateBasedCriterion(description, o => substitution(o) == description); + _subcriteria.Add(description, criterion); return criterion; } } diff --git a/src/Hosts/NVs.Budget.Hosts.Console/Commands/AdminVerb.cs b/src/Hosts/NVs.Budget.Hosts.Console/Commands/AdminVerb.cs deleted file mode 100644 index ed58a44e..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/Commands/AdminVerb.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Hosts.Console.Commands; - -[Verb("admin", HelpText = "Administrative actions")] -internal class AdminVerb() : SuperVerb([typeof(ListEffectiveSettingsVerb), typeof(PrepareDbVerb)]); diff --git a/src/Hosts/NVs.Budget.Hosts.Console/Commands/ListEffectiveSettingsVerb.cs b/src/Hosts/NVs.Budget.Hosts.Console/Commands/ListEffectiveSettingsVerb.cs deleted file mode 100644 index 6fae1443..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/Commands/ListEffectiveSettingsVerb.cs +++ /dev/null @@ -1,60 +0,0 @@ -using CommandLine; -using MediatR; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.Persistence.EF.Context; - -namespace NVs.Budget.Hosts.Console.Commands; - -[Verb("settings", HelpText = "Prints out effective settings")] -internal class ListEffectiveSettingsVerb : AbstractVerb; - -internal class ListEffectiveSettingsVerbHandler( - IOutputStreamProvider streams, - IOptions outputOptions, - IConfigurationRoot configuration, - IDbConnectionInfo dbConnectionInfo -) : IRequestHandler -{ - public async Task Handle(ListEffectiveSettingsVerb request, CancellationToken cancellationToken) - { - var writer = await streams.GetOutput(outputOptions.Value.OutputStreamName); - - await writer.WriteLineAsync("1. Output options:"); - await writer.WriteLineAsync($"{nameof(OutputOptions.OutputStreamName)}: {outputOptions.Value.OutputStreamName}"); - await writer.WriteLineAsync($"{nameof(OutputOptions.ErrorStreamName)}: {outputOptions.Value.ErrorStreamName}"); - await writer.WriteLineAsync($"{nameof(OutputOptions.ShowSuccesses)}: {outputOptions.Value.ShowSuccesses}"); - await writer.WriteLineAsync(); - - await writer.WriteLineAsync("2. Configuration sources"); - await writer.WriteLineAsync($"Working directory: {Environment.CurrentDirectory}"); - await writer.WriteLineAsync($"BUDGET_CONFIGURATION_PATH: {Environment.GetEnvironmentVariable("BUDGET_CONFIGURATION_PATH") ?? "conf.d"}"); - await writer.WriteLineAsync("Providers:"); - foreach (var provider in configuration.Providers) - { - await writer.WriteLineAsync(provider.ToString()); - } - - await writer.WriteLineAsync(); - - await writer.WriteLineAsync("3. EF Core: Database"); - await writer.WriteLineAsync($"{nameof(dbConnectionInfo.DataSource)}: {dbConnectionInfo.DataSource}"); - await writer.WriteLineAsync($"{nameof(dbConnectionInfo.Database)}: {dbConnectionInfo.Database}"); - await writer.WriteLineAsync(); - - await writer.WriteLineAsync("4. Parsing rules"); - await writer.WriteLineAsync($"CultureCode: {configuration.GetValue("CultureCode") ?? "not specified"}"); - await writer.WriteLineAsync("Known input types:"); - foreach (var section in configuration.GetSection("CsvReadingOptions").GetChildren()) - { - await writer.WriteLineAsync(section.Key); - } - - await writer.FlushAsync(cancellationToken); - - return ExitCode.Success; - } -} diff --git a/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbHandler.cs b/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbHandler.cs deleted file mode 100644 index 620e2ccb..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentResults; -using JetBrains.Annotations; -using MediatR; -using NVs.Budget.Controllers.Console.Contracts.Commands; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.Persistence.EF.Context; - -namespace NVs.Budget.Hosts.Console.Commands; - -[UsedImplicitly] -internal class PrepareDbHandler(IDbMigrator migrator, IResultWriter writer) : IRequestHandler -{ - public async Task Handle(PrepareDbVerb request, CancellationToken cancellationToken) - { - try - { - await migrator.MigrateAsync(cancellationToken); - return ExitCode.Success; - } - catch (Exception e) - { - await writer.Write(Result.Fail(new ExceptionBasedError(e)), cancellationToken); - return ExitCode.OperationError; - } - } -} diff --git a/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbVerb.cs b/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbVerb.cs deleted file mode 100644 index a70b2af3..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/Commands/PrepareDbVerb.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using NVs.Budget.Controllers.Console.Contracts.Commands; - -namespace NVs.Budget.Hosts.Console.Commands; - -[Verb("migrate-db", HelpText = "Perform database migration")] -internal class PrepareDbVerb : AbstractVerb; diff --git a/src/Hosts/NVs.Budget.Hosts.Console/ConsoleCancellationHandler.cs b/src/Hosts/NVs.Budget.Hosts.Console/ConsoleCancellationHandler.cs deleted file mode 100644 index 5d1e4d41..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/ConsoleCancellationHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace NVs.Budget.Hosts.Console; - -public class ConsoleCancellationHandler -{ - private readonly CancellationTokenSource _source = new(); - - public CancellationToken Token => _source.Token; - - public ConsoleCancellationHandler() - { - System.Console.CancelKeyPress += (_, _) => - { - _source.Cancel(true); - }; - } -} diff --git a/src/Hosts/NVs.Budget.Hosts.Console/NVs.Budget.Hosts.Console.csproj b/src/Hosts/NVs.Budget.Hosts.Console/NVs.Budget.Hosts.Console.csproj deleted file mode 100644 index bdd13717..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/NVs.Budget.Hosts.Console.csproj +++ /dev/null @@ -1,56 +0,0 @@ - - - - Exe - net8.0 - enable - enable - budget - 0.1.0 - nvsnkv - © nvsnkv 2024 - https://github.com/nvsnkv/budget - https://opensource.org/license/mit - https://github.com/nvsnkv/budget - git - budget - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - PreserveNewest - Never - - - - diff --git a/src/Hosts/NVs.Budget.Hosts.Console/Program.cs b/src/Hosts/NVs.Budget.Hosts.Console/Program.cs deleted file mode 100644 index 0996b0ee..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/Program.cs +++ /dev/null @@ -1,90 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using System.Text; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using NVs.Budget.Application; -using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Services; -using NVs.Budget.Application.UseCases; -using NVs.Budget.Controllers.Console.Handlers; -using NVs.Budget.Domain.ValueObjects; -using NVs.Budget.Hosts.Console; -using NVs.Budget.Hosts.Console.Commands; -using NVs.Budget.Infrastructure.ExchangeRates.CBRF; -using NVs.Budget.Infrastructure.Identity.Console; -using NVs.Budget.Infrastructure.IO.Console; -using NVs.Budget.Infrastructure.Persistence.EF; -using NVs.Budget.Utilities.Expressions; -using Serilog; - -Console.OutputEncoding = Encoding.UTF8; - -var cancellationHandler = new ConsoleCancellationHandler(); - -var configurationBuilder = new ConfigurationBuilder().AddJsonFile("appsettings.json"); - -var configurationDirectoryPath = Environment.GetEnvironmentVariable("BUDGET_CONFIGURATION_PATH") ?? "conf.d"; - -if (Directory.Exists(configurationDirectoryPath)) -{ - var configs = ((string[]) ["*.json", "*.yml", "*.yaml"]) - .SelectMany(p => Directory.EnumerateFiles(configurationDirectoryPath, p)) - .Order(); - - foreach (var file in configs) - { - if (file.EndsWith(".json")) - { - configurationBuilder.AddJsonFile(file); - } - else if (file.EndsWith(".yml") || file.EndsWith(".yaml")) - { - configurationBuilder.AddYamlFile(file); - } - } -} - -var configuration = configurationBuilder - .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty}.json", true) - .AddEnvironmentVariables() - .AddCommandLine(args) - .Build(); - -Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger(); - -ReadableExpressionsParser.Default.RegisterAdditionalTypes(typeof(Tag)); - -var collection = new ServiceCollection().AddConsoleIdentity() - .AddLogging(builder => builder.AddSerilog(dispose: true)) - .AddSingleton(configuration) - .AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AdminVerb).Assembly)) - .AddEfCorePersistence( - configuration.GetConnectionString("BudgetContext") ?? throw new InvalidOperationException("No connection string found for BudgetContext!"), - ReadableExpressionsParser.Default - ) - .AddScoped() - .AddScoped(p => p.GetRequiredService().CachedUser) - .AddScoped() - .AddTransient() - .AddTransient(p => p.GetRequiredService().CreateAccountant()) - .AddTransient(p => p.GetRequiredService().CreateAccountManager()) - .AddTransient(p => p.GetRequiredService().CreateReckoner()) - .AddApplicationUseCases() - .AddSingleton(new Factory().CreateProvider()) - .AddConsole() - .AddConsoleIO() - .UseConsole(configuration) - .UseConsoleIO(configuration); - -var services = collection.BuildServiceProvider(); - -var factory = services.GetRequiredService(); -using var scope = factory.CreateScope(); - -var initializer = scope.ServiceProvider.GetRequiredService(); -await initializer.TryInitializeCache(cancellationHandler.Token); - -var entryPoint = scope.ServiceProvider.GetRequiredService(); -return await entryPoint.Process(args, cancellationHandler.Token); diff --git a/src/Hosts/NVs.Budget.Hosts.Console/UserCacheInitializer.cs b/src/Hosts/NVs.Budget.Hosts.Console/UserCacheInitializer.cs deleted file mode 100644 index 155b8e15..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/UserCacheInitializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Logging; -using NVs.Budget.Application; - -namespace NVs.Budget.Hosts.Console; - -internal class UserCacheInitializer(UserCache cache, ILogger logger) -{ - public async Task TryInitializeCache(CancellationToken ct) - { - using var _ = logger.BeginScope("[User cache init]"); - { - - } - logger.LogDebug("Initializing user cache..."); - try - { - await cache.EnsureInitialized(ct); - logger.LogDebug("Cache initialized"); - } - catch (Exception e) - { - logger.LogWarning(e, "Failed to initialize cache! Most of the operations would not work properly!"); - } - } -} diff --git a/src/Hosts/NVs.Budget.Hosts.Console/appsettings.Development.json b/src/Hosts/NVs.Budget.Hosts.Console/appsettings.Development.json deleted file mode 100644 index 09e678ad..00000000 --- a/src/Hosts/NVs.Budget.Hosts.Console/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ConnectionStrings": { - "sample_BudgetContext": "User ID=postgres;Password=postgres;Host=localhost;Port=20000;Database=budgetdb;" - }, - - "OutputOptions": { - "ShowSuccesses": true - } -} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/Dockerfile b/src/Hosts/NVs.Budget.Hosts.Web.Client/Dockerfile new file mode 100644 index 00000000..1553adfd --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/Dockerfile @@ -0,0 +1,31 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM node:20 AS node-builder +COPY ["budget-client", "/client"] +RUN npm i -g @angular/cli +WORKDIR /client +RUN npm ci +RUN npm run build + + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY --exclude="budget-client" . . +RUN dotnet restore "NVs.Budget.Hosts.Web.Client.csproj" +RUN dotnet build "NVs.Budget.Hosts.Web.Client.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "NVs.Budget.Hosts.Web.Client.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +COPY --from=node-builder /client/dist/budget-client/browser /app/wwwroot +ENTRYPOINT ["dotnet", "NVs.Budget.Hosts.Web.Client.dll"] diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/NVs.Budget.Hosts.Web.Client.csproj b/src/Hosts/NVs.Budget.Hosts.Web.Client/NVs.Budget.Hosts.Web.Client.csproj new file mode 100644 index 00000000..ae3a56b2 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/NVs.Budget.Hosts.Web.Client.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + Linux + + + + + .dockerignore + + + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/Program.cs b/src/Hosts/NVs.Budget.Hosts.Web.Client/Program.cs new file mode 100644 index 00000000..75e631fc --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/Program.cs @@ -0,0 +1,14 @@ +var builder = WebApplication.CreateBuilder(); +var app = builder.Build(); + +app.UseDefaultFiles().UseStaticFiles(); +app.MapGet("/health", () => Results.Ok()); + +// Runtime configuration endpoint for Angular app +app.MapGet("/api/config", (IConfiguration configuration) => +{ + var apiUrl = configuration["ApiUrl"] ?? "https://localhost:25001"; + return Results.Ok(new { apiUrl }); +}); + +app.Run(); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/Properties/launchSettings.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/Properties/launchSettings.json new file mode 100644 index 00000000..2115ceff --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5062", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7076;http://localhost:5062", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.Development.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.dockerignore b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.dockerignore new file mode 100644 index 00000000..93fc89da --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.dockerignore @@ -0,0 +1,14 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +.env +.nyc_output +coverage +.nyc_output +.coverage +dist +.angular +.vscode +*.log \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.editorconfig b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.editorconfig new file mode 100644 index 00000000..f166060d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.gitignore b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.gitignore new file mode 100644 index 00000000..02c59d9f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db +/certificates/dev/nginx.crt +/certificates/dev/nginx.key diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/extensions.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/extensions.json new file mode 100644 index 00000000..77b37457 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/launch.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/launch.json new file mode 100644 index 00000000..925af837 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/tasks.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/tasks.json new file mode 100644 index 00000000..a298b5bd --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/README.md b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/README.md new file mode 100644 index 00000000..e17723e6 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/README.md @@ -0,0 +1,16 @@ +# BudgetClient + +Web app for Budget - online expence tracking and analytics service. + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.0. + +## Development server +In order to work with API server properly, app must be served via HTTPS. Current dev configuration expects to have `./certificates/dev/` folder with `nginx.crt` and `nginx.key` files. Please create your own certificate files prior to run app. + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `https://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/angular.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/angular.json new file mode 100644 index 00000000..a3e281a6 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/angular.json @@ -0,0 +1,116 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "budget-client": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "less" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/budget-client", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + { + "glob": "**/*", + "input": "node_modules/@taiga-ui/icons/src", + "output": "assets/taiga-ui/icons" + } + ], + "styles": [ + "src/styles.less" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "800kB", + "maximumError": "1.2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kB", + "maximumError": "10kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "budget-client:build:production" + }, + "development": { + "buildTarget": "budget-client:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "ssl": true, + "sslCert": "../../web-debug/certs/aspnetapp.crt", + "sslKey": "../../web-debug/certs/aspnetapp.key" + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package-lock.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package-lock.json new file mode 100644 index 00000000..fa6f1578 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package-lock.json @@ -0,0 +1,15510 @@ +{ + "name": "budget-client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "budget-client", + "version": "0.0.0", + "dependencies": { + "@angular/cdk": "^19.0.0", + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "@taiga-ui/addon-charts": "^4.57.0", + "@taiga-ui/addon-commerce": "^4.57.0", + "@taiga-ui/addon-table": "^4.57.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/event-plugins": "^4.7.0", + "@taiga-ui/icons": "^4.57.0", + "@taiga-ui/kit": "^4.57.0", + "@taiga-ui/layout": "^4.57.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.0", + "@angular/cli": "^19.2.0", + "@angular/compiler-cli": "^19.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1902.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.0.tgz", + "integrity": "sha512-F/3O38QOYCwNqECNQauKb56GYdST9SrRSiqTNc5xpnUL//A09kaucmKSZ2VJAVY7K/rktSQn5viiQ3rTJLiZgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.0", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.0.tgz", + "integrity": "sha512-chPiwTKQPYQn34MZ+5spTCSVSY5vha9C5UKPHsEFNiNT0Iw1mQRJkFvDyq9WZnoc4B0w5KRIiR08EjOTNHj/1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.0", + "@angular-devkit/build-webpack": "0.1902.0", + "@angular-devkit/core": "19.2.0", + "@angular/build": "19.2.0", + "@babel/core": "7.26.9", + "@babel/generator": "7.26.9", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.9", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.9", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.2.0", + "@vitejs/plugin-basic-ssl": "1.2.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.25.0", + "fast-glob": "3.3.3", + "http-proxy-middleware": "3.0.3", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.2", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "postcss": "8.5.2", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.85.0", + "sass-loader": "16.0.5", + "semver": "7.7.1", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.39.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.98.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.0", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "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" + }, + "optionalDependencies": { + "esbuild": "0.25.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.0", + "@web/test-runner": "^0.20.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1902.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.0.tgz", + "integrity": "sha512-SZsesHqrFRRUHXo4NZ1yZ+RsH/hGMVFoWb65pk+POSJYR4W6nm4pO0B2Uww2FWzv1MFfqYBOig/rBqhMB+yJ7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.0", + "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" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.0.tgz", + "integrity": "sha512-qd2nYoHZOYWRsu4MjXG8KiDtfM9ZDRR2rDGa+rDZ3CYAsngCrPmqOebun10dncUjwAidX49P4S2U2elOmX3VYQ==", + "devOptional": 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": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.0.tgz", + "integrity": "sha512-cGGqUGqBXIGJkeL65l70y0BflDAu/0Zi/ohbYat3hvadFfumRJnVElVfJ59JtWO7FfKQjxcwCVTyuQ/tevX/9A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.0", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/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==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/animations": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.0.tgz", + "integrity": "sha512-GJDwtZ+7XmAAbzCbPSJrR1iMs2l16VoA7myeVl6n5k/KsZywqb4KhPmjzLKpQlAFP0NRjg1LbHc2Fsus7/Ydag==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.0" + } + }, + "node_modules/@angular/build": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.0.tgz", + "integrity": "sha512-IdTA9SvYReNcANm0tMgEtsx8qdIqKZYoF2xPZw2YDh6TeBWZK8VwoWtpXzkOBWedf9vgcrT7y0Y8gB11pgEP6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.0", + "@babel/core": "7.26.9", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.6", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.2.0", + "browserslist": "^4.23.0", + "esbuild": "0.25.0", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.34.8", + "sass": "1.85.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "vite": "6.1.0", + "watchpack": "2.4.2" + }, + "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" + }, + "optionalDependencies": { + "lmdb": "3.2.6" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/vite": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.5.1", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/vite/node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/@angular/cdk": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz", + "integrity": "sha512-j7dg18PJIbyeU4DTko3vIK3M2OuUv3H0ZViNddOaLlGN5X93cq4QCGcNhcGm3x3r5rUr/AaexYu+KHMyN8PwmA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^20.0.0", + "@angular/core": "^19.0.0 || ^20.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.0.tgz", + "integrity": "sha512-LUxuku6obwigdDJozAvmXyhMcm3rSXFoZK4+Al7r/JE80pjQEE+bGpu7jCb6JsH813DTNauN+BB66qk8bXSgRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.0", + "@angular-devkit/core": "19.2.0", + "@angular-devkit/schematics": "19.2.0", + "@inquirer/prompts": "7.3.2", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.2.0", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.2", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.10", + "semver": "7.7.1", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.0.tgz", + "integrity": "sha512-dm8PR94QY3DucXxltdV5p2Yxyr5bfPlmjOElwLhiTvxWbwCZJTVhPc8dw0TCKzCEu+tKafT48u4BLIB34a0A/g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.0.tgz", + "integrity": "sha512-xGBD0C9ikH4jVDuQU3XzGqbh9Wovl8UR0wNzNd9rm4fltfC9ipz9NbfetsLPKWpPbfnUqmqMe4/pYjGEgWMonw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.0.tgz", + "integrity": "sha512-IFl3LNfFanspS4gHjn207TPuoJGGieuC9r+j3nDitUcFH49fbShYLGCB6xczvK+j68ZWCqv4voxAOmLyfA/Opw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.2.0", + "typescript": ">=5.5 <5.9" + } + }, + "node_modules/@angular/core": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.0.tgz", + "integrity": "sha512-WKTRltOt3MMWWuhRX7Y9RonKxIYjZeBDE6XRwceHMgaEDS2d8I2D3AIuqizRsgHpJqDPnQnH+vxcek4FivcSGA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.0.tgz", + "integrity": "sha512-/GHQgiDPUr1vMXCB1O8c+O70DcoZykDBzOICCaz3kTu46rp48g6E6iaZVJoozI0iBwB8+rnuTPQnLWJ46w+wVg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.0", + "@angular/core": "19.2.0", + "@angular/platform-browser": "19.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.0.tgz", + "integrity": "sha512-rt3byGZWU0jF6QCLxjP+LH94uL0VM5LgtJ+tYclJqCNB1C3fZrpa86GVd9onVbZmDk0ETUOwm7dQHYdef8oiqw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.2.0", + "@angular/common": "19.2.0", + "@angular/core": "19.2.0" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.0.tgz", + "integrity": "sha512-664OAYxzRYx9WjZu+o0VT+vMM0OqPOb0OnbwnyvIVNBuufWK7/IxWZ/U+Kh9A/XJYpDPtB5N1WEfeiO8AAzWnQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.0", + "@angular/compiler": "19.2.0", + "@angular/core": "19.2.0", + "@angular/platform-browser": "19.2.0" + } + }, + "node_modules/@angular/router": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.0.tgz", + "integrity": "sha512-Md/zleBpWMi5H6KPMREM0M2EUAkoqe01zkXla0Z0hHoTn7Ty0fv0Te9bGDioVOG7JgHh6wYCrPJ/uJsjKObyvw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.0", + "@angular/core": "19.2.0", + "@angular/platform-browser": "19.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/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/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz", + "integrity": "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.26.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.9.tgz", + "integrity": "sha512-Jf+8y9wXQbbxvVYTM8gO5oEF2POdNji0NMltEkG7FtmzD9PVz7/lxpqSdTvwsjTMU5HIHuDVNf2SOxLkWi+wPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.2.tgz", + "integrity": "sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.7.tgz", + "integrity": "sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.7.tgz", + "integrity": "sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.9.tgz", + "integrity": "sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.6.tgz", + "integrity": "sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.9.tgz", + "integrity": "sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.9.tgz", + "integrity": "sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.9.tgz", + "integrity": "sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.9.tgz", + "integrity": "sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.9.tgz", + "integrity": "sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.4.tgz", + "integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", + "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", + "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", + "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", + "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", + "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", + "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@maskito/angular": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.11.1.tgz", + "integrity": "sha512-+OZzbRJj/9fOGhgPr0xYctSHe/Ngahip3VdNWBslRTpt7g+UTBYcB8vU9J4cHfpdXYeLM3tM0tnKksc3Eis0+Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "@maskito/core": "^3.11.1" + } + }, + "node_modules/@maskito/core": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.11.1.tgz", + "integrity": "sha512-zN5k/BiZXblo8mEFhsGnnXBCqKMkjEGArorOOcpB1/ymZyqF12Dk6IipEsSE6abMnWw4YF2tukzfq73BFZKz8A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@maskito/kit": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.11.1.tgz", + "integrity": "sha512-KOBUqxRz383xJWCoe+Emwxv2oAzUrZobIN+Gntmi5Py2S10XbqYnGX/6W7QHN8CUK2Nx11d3HsxbEQaq5Hinjg==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@maskito/core": "^3.11.1" + } + }, + "node_modules/@maskito/phone": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.11.1.tgz", + "integrity": "sha512-ptNDPIZQs/v598qydBa9cnvoCE8+k2Sv07kKKVx3vG0V40DQnIlEL+LYKrJJbMIiPOB6CH90hB9eaA9KKReZ6w==", + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@maskito/core": "^3.11.1", + "@maskito/kit": "^3.11.1", + "libphonenumber-js": ">=1.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ng-web-apis/common": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.12.2.tgz", + "integrity": "sha512-fjmSpJfu+3i7i/+2U38owTTxoi8EoANQJVaIGmNymyF8EmSrbVCcPP6CXsZ6UsHJwE8IWso7h0cN6kBUqWERfw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ng-web-apis/intersection-observer": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.12.2.tgz", + "integrity": "sha512-hE2jBuRpn/FBcbOZuYwMG/uGW4Y7T+T0g3MiY3eCY+YSZQ5wWtzHdkj8bpdAaAUbHuKJuxtzQG68uMd8yVUMMg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": ">=4.12.0" + } + }, + "node_modules/@ng-web-apis/mutation-observer": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.12.2.tgz", + "integrity": "sha512-LEbtahyyBp/LhPgxAsjY3G75bqeWmBTMHYiMH4kHa8/ExH0LSKplB/X9nadUJ9YdMQ8d/DDC9cxQvIozOzcyRA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": ">=4.12.0" + } + }, + "node_modules/@ng-web-apis/platform": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.12.2.tgz", + "integrity": "sha512-HO2nm1us87iiOntXwEGv1D5L1kQ4luJlOcWnCPjEwiWE4vxANDis/MnYvJAcwAUQRNikXw8BjgAEo3aWafxDhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@ng-web-apis/resize-observer": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.12.2.tgz", + "integrity": "sha512-ogthPEuHRBaJCXv/Df8GCqag52WkKziKx4l/+jp2jmaxKHWtmzW59Ftb7HFfoCzmkb+tVlVdC3EwFyldax2eyw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": ">=4.12.0" + } + }, + "node_modules/@ng-web-apis/screen-orientation": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.12.2.tgz", + "integrity": "sha512-8ZKFrfu0/7coUECmqi+ppk/1lUfHi38N0msmKR+iZvVbDiZcaJhu2SptZEmEkR1pTykp1wbbKSSfdyDb3MK1hQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": ">=4.12.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.0.tgz", + "integrity": "sha512-63/8ys3bNK2h7Py/dKHZ4ZClxQz6xuz3skUgLZIMs9O076KPsHTKDKEDG2oicmwe/nOXjVt6n9Z4wprFaRLbvw==", + "dev": true, + "license": "MIT", + "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": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "typescript": ">=5.5 <5.9", + "webpack": "^5.54.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/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/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/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/@npmcli/git/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", + "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.1.1.tgz", + "integrity": "sha512-3Hc2KGIkrvJWJqTbvueXzBeZlmvoOxc2jyX00yzr3+sNFquJg0N8hH4SAPLPVrkWIRQICVpVgjrss971awXVnA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.0.2.tgz", + "integrity": "sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.0.tgz", + "integrity": "sha512-/gdrYTr1DSUNmrUmpmne6uBnIBpJ/obHtccvz5sZckKni/KMPAr3CgGZ8JrHer3I732ucb1We9nbdtXvz+2glg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.0", + "@angular-devkit/schematics": "19.2.0", + "jsonc-parser": "3.3.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/@sigstore/bundle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.0.tgz", + "integrity": "sha512-o09cLSIq9EKyRXwryWDOJagkml9XgQCoCSRjHOnHLnvsivaW7Qznzz6yjfV7PHJHhIvyp8OH7OX8w0Dc5bQK7A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.0.tgz", + "integrity": "sha512-suVMQEA+sKdOz5hwP9qNcEjX6B45R+hFFr4LAWzbRc5O+U2IInwvay/bpG5a4s+qR35P/JK/PiKiRGjfuLy1IA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.0.tgz", + "integrity": "sha512-kAAM06ca4CzhvjIZdONAL9+MLppW3K48wOFy1TbuaWFW/OMfl8JuTgW0Bm02JB1WJGT/ET2eqav0KTEKmxqkIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@taiga-ui/addon-charts": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.57.0.tgz", + "integrity": "sha512-IKLqx9Wx0YdYirnfD8SGXusntR1jXiKKzeH5kqeeM25kJ6gHxTM/MZ4hmRtm89PTUd53E0HSkZg0K98vAC4tEA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": "^4.12.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0" + } + }, + "node_modules/@taiga-ui/addon-commerce": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.57.0.tgz", + "integrity": "sha512-SaZo/O4jmBnQLroJUyrseOQNnPv0ZFJQhGGcOzZbN+hoYnMsHwEB/Ps/e/pImRowZ/b1U7WvDzoi2CG3pF0tDQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "@maskito/angular": "^3.11.1", + "@maskito/core": "^3.11.1", + "@maskito/kit": "^3.11.1", + "@ng-web-apis/common": "^4.12.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/i18n": "^4.57.0", + "@taiga-ui/kit": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/addon-table": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.57.0.tgz", + "integrity": "sha512-0G83gRhKTowcqqCgIPx0ula6TTTQRUEuiLYlUtAynduxh/Z6mFyjd3/lZywVqt5+dJwu+pNH9W3W2co4eSlpGg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ng-web-apis/intersection-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/i18n": "^4.57.0", + "@taiga-ui/kit": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/cdk": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.57.0.tgz", + "integrity": "sha512-34vktv6+IFLbFHdswF3mExVg+baqAf29WH1MujY7GfR0KDdAOJ3bpMj8ecmsp6Uap4dkEWxHDvJgKHy6y5aIJQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.8.1" + }, + "optionalDependencies": { + "@angular-devkit/core": ">=16.0.0", + "@angular-devkit/schematics": ">=16.0.0", + "@schematics/angular": ">=16.0.0", + "ng-morph": "^4.8.4", + "parse5": "^8.0.0" + }, + "peerDependencies": { + "@angular/animations": ">=16.0.0", + "@angular/cdk": ">=16.0.0", + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@ng-web-apis/platform": "^4.12.0", + "@ng-web-apis/resize-observer": "^4.12.0", + "@ng-web-apis/screen-orientation": "^4.12.0", + "@taiga-ui/event-plugins": "^4.7.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@taiga-ui/core": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.57.0.tgz", + "integrity": "sha512-XH95qBHQa5rXpicDR8bRu1NE14GAamcn7DUFxwIymRZkuymzGhvlhh4sUvcl8uaBzDXH7FegPeQsMveGRFSjrg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/animations": ">=16.0.0", + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "@angular/platform-browser": ">=16.0.0", + "@angular/router": ">=16.0.0", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/event-plugins": "^4.7.0", + "@taiga-ui/i18n": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/event-plugins": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.7.0.tgz", + "integrity": "sha512-j3HPRPR7XxKxgMeytb+r/CNUoLBMVrfdfL8KJr1XiFO9jyEvoC4chFXDXWlkGyUHJIC6wy5VIXlIlI/kpqOiGg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@angular/platform-browser": ">=16.0.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/i18n": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.57.0.tgz", + "integrity": "sha512-pXHIEvQlVyylil2nLgHLd1CRCCmusgnH/UjJKlbo98ozJiqD2FgPu/VnaADrsKY27Mb/9uG1ouKxu2Y+4H2q+A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@ng-web-apis/common": "^4.12.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/icons": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.57.0.tgz", + "integrity": "sha512-CsquOCtpPk4x7DHP0dP6w6lx4/uw+BwzxUgm781LEk+/E56EZgvrYZ1qPCMnRXsmqnHFuS5oMXQzCdvHB0S6Tg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@taiga-ui/kit": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.57.0.tgz", + "integrity": "sha512-cbPA8pA35MynldQvA7m7nrSFxWk/RMq3LBUyJLg4mccRnJfA4TNBQFs9BGn0va2enru57EJwFKOdBzhWvJcjiA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "@angular/router": ">=16.0.0", + "@maskito/angular": "^3.11.1", + "@maskito/core": "^3.11.1", + "@maskito/kit": "^3.11.1", + "@maskito/phone": "^3.11.1", + "@ng-web-apis/common": "^4.12.0", + "@ng-web-apis/intersection-observer": "^4.12.0", + "@ng-web-apis/mutation-observer": "^4.12.0", + "@ng-web-apis/resize-observer": "^4.12.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/i18n": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/layout": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.57.0.tgz", + "integrity": "sha512-ULEWXOLdZvuqyLsyx4zcBUjfe1+f8gWmwrKV1ZNmFfCQdxIBHwaaNb+h84CSiW0koQm4ghDfRWm1JLOTpG9B1A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": ">=2.8.1" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/kit": "^4.57.0", + "@taiga-ui/polymorpheus": "^4.9.0", + "rxjs": ">=7.0.0" + } + }, + "node_modules/@taiga-ui/polymorpheus": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz", + "integrity": "sha512-TbIIwslbEnxunKuL9OyPZdmefrvJEK6HYiADEKQHUMUs4Pk2UbhMckUieURo83yPDamk/Mww+Nu/g60J/4uh2w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "@angular/platform-browser": ">=16.0.0" + } + }, + "node_modules/@ts-morph/common": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", + "integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-glob": "^3.3.2", + "minimatch": "^9.0.4", + "mkdirp": "^3.0.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.7.tgz", + "integrity": "sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/node": { + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz", + "integrity": "sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=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==", + "devOptional": 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/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/beasties": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", + "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.1.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "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": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-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/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/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/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "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/caniuse-lite": { + "version": "1.0.30001701", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", + "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "devOptional": 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/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/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/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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT", + "optional": true + }, + "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==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "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==", + "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" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.109", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz", + "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/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", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.0.tgz", + "integrity": "sha512-60iuWr6jdTVylmGXjpnqk3pCktUi5Rmjiv6EMza3h4X20BLtfL2BjUGs1+UCt2G9UK7jVGrJdUr5i1k0sL3wBg==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "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/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "devOptional": 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.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", + "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/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/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/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/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz", + "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/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/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/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/karma/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/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/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/karma/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/karma/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/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/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/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", + "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.23", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.23.tgz", + "integrity": "sha512-RN3q3gImZ91BvRDYjWp7ICz3gRn81mW5L4SW+2afzNCC0I/nkXstBgZThQGTE3S/9q5J90FH4dP+TXx8NhdZKg==", + "license": "MIT", + "peer": true + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", + "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.6", + "@lmdb/lmdb-darwin-x64": "3.2.6", + "@lmdb/lmdb-linux-arm": "3.2.6", + "@lmdb/lmdb-linux-arm64": "3.2.6", + "@lmdb/lmdb-linux-x64": "3.2.6", + "@lmdb/lmdb-win32-x64": "3.2.6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.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/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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minizlib/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/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", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ng-morph": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-4.8.4.tgz", + "integrity": "sha512-XwL53wCOhyaAxvoekN74ONbWUK30huzp+GpZYyC01RfaG2AX9l7YlC1mGG/l7Rx7YXtFAk85VFnNJqn2e46K8g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "jsonc-parser": "3.3.1", + "minimatch": "10.0.1", + "multimatch": "5.0.0", + "ts-morph": "23.0.0" + }, + "peerDependencies": { + "@angular-devkit/core": ">=16.0.0", + "@angular-devkit/schematics": ">=16.0.0", + "tslib": "^2.7.0" + } + }, + "node_modules/ng-morph/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ng-morph/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.1.0.tgz", + "integrity": "sha512-/+7TuHKnBpnMvUQnsYEb0JOozDZqarQbfNuSGLXIjhStMT0fbw7IdSqWgopOP5xhRZE+lsbIvAHcekddruPZgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/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==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/ora/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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/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/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT", + "optional": true + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/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/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "devOptional": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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/sass": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "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/schema-utils/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/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/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": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/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/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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "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-cjs/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/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.12.tgz", + "integrity": "sha512-jDLYqo7oF8tJIttjXO6jBY5Hk8p3A8W4ttih7cCEq64fQFWmgJ4VqAQjKr7WwIDlmXKEc6QeoRb5ecjZ+2afcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-morph": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz", + "integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ts-morph/common": "~0.24.0", + "code-block-writer": "^13.0.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webpack": { + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "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.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.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/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/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/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "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/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/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/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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/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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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-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/wrap-ansi/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/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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/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/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/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.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/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/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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", + "license": "MIT" + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package.json new file mode 100644 index 00000000..ef2e91ba --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/package.json @@ -0,0 +1,47 @@ +{ + "name": "budget-client", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/cdk": "^19.0.0", + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "@taiga-ui/addon-charts": "^4.57.0", + "@taiga-ui/addon-commerce": "^4.57.0", + "@taiga-ui/addon-table": "^4.57.0", + "@taiga-ui/cdk": "^4.57.0", + "@taiga-ui/core": "^4.57.0", + "@taiga-ui/event-plugins": "^4.7.0", + "@taiga-ui/icons": "^4.57.0", + "@taiga-ui/kit": "^4.57.0", + "@taiga-ui/layout": "^4.57.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.0", + "@angular/cli": "^19.2.0", + "@angular/compiler-cli": "^19.2.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.7.2" + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/public/favicon.ico b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/public/favicon.ico new file mode 100644 index 00000000..69d3bdd3 Binary files /dev/null and b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/public/favicon.ico differ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.html new file mode 100644 index 00000000..a2c9b71e --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.html @@ -0,0 +1,26 @@ + +
+ + The Budget. + + + + + + {{ownerName$ | async}} + + + +
+
+ +
+
\ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.less new file mode 100644 index 00000000..05d3a7b0 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.less @@ -0,0 +1,10 @@ +.flex-space-between { + display:flex; + justify-content: space-between; +} + +.flex { + display:flex; + align-items: center; + gap: 1rem; +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.spec.ts new file mode 100644 index 00000000..5c868949 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import { TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'budget-client' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('budget-client'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, budget-client'); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.ts new file mode 100644 index 00000000..404be52d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.component.ts @@ -0,0 +1,37 @@ +import { TuiRoot, TuiButton, TuiIcon } from "@taiga-ui/core"; +import { TuiBlockStatus, TuiNavigation } from "@taiga-ui/layout" +import { Component, enableProdMode } from '@angular/core'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { AuthComponent } from './auth/auth/auth.component'; +import { environment } from '../environments/environment'; +import { UserService } from "./auth/user.service"; +import { CommonModule } from "@angular/common"; +import { map } from "rxjs"; +import { BudgetSelectorComponent } from "./budget/budget-selector/budget-selector.component"; +import { ThemeService } from "./theme.service"; + +if (environment.production) { + enableProdMode(); +} + +@Component({ + selector: 'app-root', + imports: [RouterOutlet, AuthComponent, TuiRoot, TuiNavigation, TuiBlockStatus, CommonModule, RouterLink, BudgetSelectorComponent, TuiButton, TuiIcon], + templateUrl: './app.component.html', + styleUrl: './app.component.less' +}) +export class AppComponent { + title = 'budget-client'; + + get currentUser$() { return this.user.current$; } + get isAuthenticated$() { return this.user.current$.pipe(map(u => u.isAuthenticated)); } + get userId$() { return this.user.current$.pipe(map(u => u.id)); } + get ownerName$() { return this.user.current$.pipe(map(u => u.ownerInfo?.name)); } + get isDarkTheme$() { return this.theme.isDark$; } + + constructor(private user: UserService, private theme: ThemeService) { } + + toggleTheme(): void { + this.theme.toggleTheme(); + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.config.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.config.ts new file mode 100644 index 00000000..a3af3ea6 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.config.ts @@ -0,0 +1,19 @@ +import { provideEventPlugins } from "@taiga-ui/event-plugins"; +import { provideAnimations } from "@angular/platform-browser/animations"; +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { appConfigInitializerProvider } from './config/app-config.initializer'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAnimations(), + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideHttpClient(withInterceptorsFromDi()), + provideEventPlugins(), + appConfigInitializerProvider + ] +}; diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.routes.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.routes.ts new file mode 100644 index 00000000..ce7adfb0 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/app.routes.ts @@ -0,0 +1,29 @@ +import { Routes } from '@angular/router'; +import { NewBudgetComponent } from './budget/new-budget/new-budget.component'; +import { BudgetDetailComponent } from './budget/budget-detail/budget-detail.component'; +import { ReadingSettingsComponent } from './budget/reading-settings/reading-settings.component'; +import { ImportOperationsComponent } from './operations/import-operations/import-operations.component'; +import { OperationsListComponent } from './operations/operations-list/operations-list.component'; +import { DeleteOperationsComponent } from './operations/delete-operations/delete-operations.component'; +import { RetagOperationsComponent } from './operations/retag-operations/retag-operations.component'; +import { LogbookViewComponent } from './operations/logbook-view/logbook-view.component'; +import { LogbookGroupComponent } from './operations/logbook-group/logbook-group.component'; +import { DuplicatesListComponent } from './operations/duplicates-list/duplicates-list.component'; +import { TransfersListComponent } from './operations/transfers-list/transfers-list.component'; +import { IndexComponent } from './index/index.component'; + +export const routes: Routes = [ + { path: 'budget/new', component: NewBudgetComponent }, + { path: 'budget/:budgetId/operations/import', component: ImportOperationsComponent }, + { path: 'budget/:budgetId/operations/delete', component: DeleteOperationsComponent }, + { path: 'budget/:budgetId/operations/retag', component: RetagOperationsComponent }, + { path: 'budget/:budgetId/operations/logbook/group', component: LogbookGroupComponent }, + { path: 'budget/:budgetId/operations/logbook', component: LogbookViewComponent }, + { path: 'budget/:budgetId/operations/duplicates', component: DuplicatesListComponent }, + { path: 'budget/:budgetId/transfers', component: TransfersListComponent }, + { path: 'budget/:budgetId/operations', component: OperationsListComponent }, + { path: 'budget/:budgetId/details', component: BudgetDetailComponent }, + { path: 'budget/:budgetId/reading-settings', component: ReadingSettingsComponent }, + { path: 'budget/:budgetId', redirectTo: 'budget/:budgetId/operations', pathMatch: 'full' }, + { path: '', component: IndexComponent } +]; diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth-response.model.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth-response.model.ts new file mode 100644 index 00000000..9576d484 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth-response.model.ts @@ -0,0 +1,10 @@ +export interface AuthResponse { + isAuthenticated: boolean; + user?: { + id: string + }; + owner?: { + id: string; + name: string; + } + } \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.html new file mode 100644 index 00000000..7b8d6fe6 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.html @@ -0,0 +1,2 @@ +Logout +Login \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.less new file mode 100644 index 00000000..33761d1a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.less @@ -0,0 +1,3 @@ +a { + padding-left: 5pt; +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.spec.ts new file mode 100644 index 00000000..365dab28 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthComponent } from './auth.component'; + +describe('AuthComponent', () => { + let component: AuthComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AuthComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AuthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.ts new file mode 100644 index 00000000..4305ffee --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.component.ts @@ -0,0 +1,49 @@ +// auth-status.component.ts +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; +import { CommonModule } from '@angular/common'; +import { UserService } from '../user.service'; +import { TuiLink } from '@taiga-ui/core'; + +@Component({ + selector: 'app-auth', + templateUrl: './auth.component.html', + styleUrls: ['./auth.component.less'], + imports: [CommonModule, TuiLink], +}) +export class AuthComponent implements OnInit { + isAuthenticated = false; + + constructor( + private authService: AuthService, + private user: UserService + ) {} + + ngOnInit() { + // Base URL is already set by APP_INITIALIZER + this.checkAuthentication(); + } + + get BaseUrl() { + return this.authService.BaseUrl; + } + + checkAuthentication() { + this.authService.whoAmI().subscribe(response => { + this.isAuthenticated = response.isAuthenticated; + if (response.isAuthenticated) { + this.user.setCurrentUser({ + isAuthenticated: true, + id: response.user!.id, + ownerInfo: response.owner + }) + } + else { + this.user.setCurrentUser({ + isAuthenticated: false + }) + } + }); + } +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.service.ts new file mode 100644 index 00000000..d19f4865 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/auth/auth.service.ts @@ -0,0 +1,28 @@ +// auth.service.ts +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AuthResponse } from './auth-response.model'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private baseUrl: string = ""; + + constructor(private http: HttpClient) {} + + get BaseUrl(): string { + return this.baseUrl; + } + + setBaseUrl(url: string): void { + this.baseUrl = url; + } + + whoAmI(): Observable { + return this.http.get(this.buildUrl('auth/whoami'), { withCredentials: true }); + } + + private buildUrl(endpoint: string): string { + return `${this.baseUrl}/${endpoint}`; + } +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.spec.ts new file mode 100644 index 00000000..3f804c9f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.ts new file mode 100644 index 00000000..062e9ba7 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/auth/user.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + private readonly user = new BehaviorSubject({isAuthenticated: false}); + + get current$() { return this.user.asObservable(); } + + constructor() { } + + setCurrentUser(user: User) { this.user.next(user); } +} + +export interface User { + isAuthenticated: boolean; + + id?: string; + ownerInfo?: Owner; +} + +export interface Owner { + id: string; + name: string; +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.spec.ts new file mode 100644 index 00000000..3d4ac049 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.spec.ts @@ -0,0 +1,77 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { BudgetApiService } from './budget-api.service'; +import { BudgetResponse, CreateBudgetRequest, UpdateBudgetRequest } from './models'; + +describe('BudgetApiService', () => { + let service: BudgetApiService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [BudgetApiService, provideHttpClientTesting()] + }); + + service = TestBed.inject(BudgetApiService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); // Проверяем, что все ожидаемые запросы были выполнены + }); + + it('should get all budgets', () => { + const mockData: BudgetResponse[] = [ + { id: '123', name: 'Test Budget' }, + { id: '456', name: 'Another Budget' } + ]; + + service.getAllBudgets().subscribe((data: BudgetResponse[]) => { + expect(data).toEqual(mockData); // Проверка, что возвращаемые данные совпадают с ожидаемыми + }); + + const req = httpMock.expectOne({ method: 'GET', url: '/api/v0.1/Budget' }); + expect(req.request.withCredentials).toBe(true); // Проверяем, что с запросом отправляются куки + req.flush(mockData); // Эмулируем успешный ответ сервера + }); + + it('should create a new budget', () => { + const request: CreateBudgetRequest = { name: 'New Budget' }; + const expectedResponse: BudgetResponse = { id: '789', name: 'New Budget' }; + + service.createBudget(request).subscribe((response: BudgetResponse) => { + expect(response).toEqual(expectedResponse); + }); + + const req = httpMock.expectOne({ method: 'POST', url: '/api/v0.1/Budget' }); + expect(req.request.body).toEqual(request); // Проверяем, что тело запроса совпадает с ожидаемым + expect(req.request.headers.has('Content-Type')).toBeTruthy(); // Проверяем наличие заголовка Content-Type + expect(req.request.withCredentials).toBe(true); // Проверяем, что с запросом отправляются куки + req.flush(expectedResponse); // Эмулируем успешный ответ сервера + }); + + it('should update an existing budget', () => { + const id = '123'; + const request: UpdateBudgetRequest = { id, name: 'Updated Budget' }; + + service.updateBudget(id, request).subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'PUT', url: `/api/v0.1/Budget/${id}` }); + expect(req.request.body).toEqual(request); // Проверяем, что тело запроса совпадает с ожидаемым + expect(req.request.withCredentials).toBe(true); // Проверяем, что с запросом отправляются куки + req.flush(null); // Эмулируем успешный ответ сервера (без тела) + }); + + it('should get a specific budget by id', () => { + const id = '123'; + const mockData: BudgetResponse = { id: '123', name: 'Test Budget' }; + + service.getBudgetById(id).subscribe((data) => { + expect(data).toEqual(mockData); // Проверка, что возвращаемые данные совпадают с ожидаемыми + }); + + const req = httpMock.expectOne({ method: 'GET', url: `/api/v0.1/Budget/${id}` }); + expect(req.request.withCredentials).toBe(true); // Проверяем, что с запросом отправляются куки + req.flush(mockData); // Эмулируем успешный ответ сервера + }); +}); \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.ts new file mode 100644 index 00000000..f956348a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-api.service.ts @@ -0,0 +1,184 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { BehaviorSubject, Observable, startWith, switchMap, tap } from 'rxjs'; +import { + BudgetResponse, + RegisterBudgetRequest, + UpdateBudgetRequest, + ChangeBudgetOwnersRequest, + MergeBudgetsRequest, + IError, + FileReadingSettingResponse +} from './models'; +import { AppConfigService } from '../config/app-config.service'; + +@Injectable({ + providedIn: 'root' +}) +export class BudgetApiService { + public readonly baseUrl: string; + private refresh$ = new BehaviorSubject(false); + + constructor( + private http: HttpClient, + private configService: AppConfigService + ) { + this.baseUrl = this.configService.apiUrl + '/api/v0.1'; + } + + /** + * Get all budgets available to the current user + */ + getAllBudgets(): Observable { + return this.refresh$.pipe( + startWith(undefined), + switchMap(() => + this.http.get(`${this.baseUrl}/budget`, { withCredentials: true }) + )); + } + + /** + * Get budget by ID (from list) + */ + getBudgetById(id: string): Observable { + return this.getAllBudgets().pipe( + switchMap(budgets => [budgets.find(b => b.id === id)]) + ); + } + + /** + * Register a new budget + */ + createBudget(request: RegisterBudgetRequest): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/json'); + return this.http.post(`${this.baseUrl}/budget`, request, { + headers, + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Update an existing budget + */ + updateBudget(id: string, request: UpdateBudgetRequest): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/json'); + return this.http.put(`${this.baseUrl}/budget/${id}`, request, { + headers, + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Change budget owners + */ + changeBudgetOwners(request: ChangeBudgetOwnersRequest): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/json'); + return this.http.put(`${this.baseUrl}/budget/owners`, request, { + headers, + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Remove a budget + */ + removeBudget(id: string, version: string): Observable { + return this.http.delete(`${this.baseUrl}/budget/${id}?version=${encodeURIComponent(version)}`, { + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Merge multiple budgets + */ + mergeBudgets(request: MergeBudgetsRequest): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/json'); + return this.http.post(`${this.baseUrl}/budget/merge`, request, { + headers, + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Download budget configuration as YAML + */ + downloadBudgetYaml(id: string): Observable { + return this.http.get(`${this.baseUrl}/budget/${id}`, { + responseType: 'blob', + headers: new HttpHeaders().set('Accept', 'application/yaml'), + withCredentials: true + }); + } + + /** + * Upload budget configuration from YAML content + */ + uploadBudgetYaml(id: string, yamlContent: string): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/yaml'); + return this.http.put(`${this.baseUrl}/budget/${id}`, yamlContent, { + headers, + withCredentials: true + }).pipe(tap(() => this.refresh$.next(true))); + } + + /** + * Download CSV reading options as YAML + */ + downloadCsvOptionsYaml(id: string): Observable { + return this.http.get(`${this.baseUrl}/budget/${id}/csv-options.yaml`, { + responseType: 'blob', + withCredentials: true + }); + } + + /** + * Upload CSV reading options from YAML file + */ + uploadCsvOptionsYaml(id: string, file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return this.http.put(`${this.baseUrl}/budget/${id}/csv-options`, formData, { withCredentials: true }); + } + + /** + * Get file reading settings for a budget + */ + getReadingSettings(budgetId: string): Observable> { + return this.http.get>(`${this.baseUrl}/budget/${budgetId}/reading-settings`, { + withCredentials: true + }); + } + + /** + * Update file reading settings for a budget + */ + updateReadingSettings(budgetId: string, settings: Record): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/json'); + return this.http.put(`${this.baseUrl}/budget/${budgetId}/reading-settings`, settings, { + headers, + withCredentials: true + }); + } + + /** + * Download reading settings as YAML + */ + downloadReadingSettingsYaml(budgetId: string): Observable { + return this.http.get(`${this.baseUrl}/budget/${budgetId}/reading-settings`, { + responseType: 'blob', + headers: new HttpHeaders().set('Accept', 'application/yaml'), + withCredentials: true + }); + } + + /** + * Upload reading settings from YAML content + */ + uploadReadingSettingsYaml(budgetId: string, yamlContent: string): Observable { + const headers = new HttpHeaders().set('Content-Type', 'application/yaml'); + return this.http.put(`${this.baseUrl}/budget/${budgetId}/reading-settings`, yamlContent, { + headers, + withCredentials: true + }); + } +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.html new file mode 100644 index 00000000..42eaf56a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.html @@ -0,0 +1,882 @@ +
+ @if (budget$ | async; as budget) { + +
+ +

{{ budget.name }}

+
+ @if (!isEditMode) { + + + + + + + } +
+ + +
+
+
+ + {{ budget.id }} +
+
+ + {{ budget.version }} +
+
+ + +
+

Owners

+
+ @for (owner of budget.owners; track owner.id) { + + {{ owner.name }} + + } +
+
+
+ + + @if (isEditMode && budgetForm) { +
+ +
+

Basic Information

+ + + + +
+ + +
+
+
+

Tagging Criteria

+ +
+ @if (showTaggingCriteria) { + + } +
+ + @if (showTaggingCriteria) { + +
+ @for (criterion of taggingCriteria.controls; track $index; let i = $index) { + +
+ Tagging Criterion {{ i + 1 }} + +
+ +
+ + + + + + + + + +
+
+ } +
+
+ } +
+ + +
+
+
+

Transfer Criteria

+ +
+ @if (showTransferCriteria) { + + } +
+ + @if (showTransferCriteria) { + +
+ @for (criterion of transferCriteria.controls; track $index; let i = $index) { + +
+ Transfer Criterion {{ i + 1 }} + +
+ +
+ + + + + + + + + + + + + + +
+
+ } +
+
+ } +
+ + +
+
+
+

Logbook Criteria

+ +
+
+ + @if (showLogbookCriteria) { +
+ + + + + +
+ +
+ + + +
+
+ + + @if (logbookCriteria.get('criteriaType')?.value === 'group') { +
+ +
+ } + + + @if (logbookCriteria.get('criteriaType')?.value === 'tag-based') { +
+ + +
+ + + + + + } + + + @if (logbookCriteria.get('criteriaType')?.value === 'criteria-based') { + + + + + } + + + @if (logbookCriteria.get('criteriaType')?.value === 'group' && !logbookCriteria.get('isUniversal')?.value) { + + + + + } + + + @if (logbookCriteria.get('criteriaType')?.value !== 'group' || !logbookCriteria.get('isUniversal')?.value) { + + + + + } + + +
+

Subcriteria

+ + + @for (subcriterion of getSubcriteria(logbookCriteria).controls; track $index; let idx = $index) { +
+
+ Subcriterion {{ idx + 1 }} + +
+ + + +
+ + + + + +
+ +
+ + + +
+
+ + + @if (subcriterion.get('criteriaType')?.value === 'group') { +
+ +
+ } + + + @if (subcriterion.get('criteriaType')?.value === 'tag-based') { +
+ + +
+ + + + + + } + + + @if (subcriterion.get('criteriaType')?.value === 'criteria-based') { + + + + + } + + + @if (subcriterion.get('criteriaType')?.value === 'group' && !subcriterion.get('isUniversal')?.value) { + + + + + } + + + @if (subcriterion.get('criteriaType')?.value !== 'group' || !subcriterion.get('isUniversal')?.value) { + + + + + } + + +
+
Nested Subcriteria
+ + + @for (nestedSub of getSubcriteria($any(subcriterion)).controls; track $index; let nestedIdx = $index) { +
+ +
+ Nested {{ nestedIdx + 1 }} + +
+ + +
+ + + + + +
+ +
+ + + +
+
+ + + @if (nestedSub.get('criteriaType')?.value === 'group') { +
+ +
+ } + + @if (nestedSub.get('criteriaType')?.value === 'tag-based') { +
+ + +
+ + + + + } + + @if (nestedSub.get('criteriaType')?.value === 'criteria-based') { + + + + + } + + @if (nestedSub.get('criteriaType')?.value === 'group' && !nestedSub.get('isUniversal')?.value) { + + + + + } + + @if (nestedSub.get('criteriaType')?.value !== 'group' || !nestedSub.get('isUniversal')?.value) { + + + + + } + + +
+
Level 4 Subcriteria
+ + + @for (level4Sub of getSubcriteria($any(nestedSub)).controls; track $index; let l4Idx = $index) { +
+ +
+ Level 4 - {{ l4Idx + 1 }} + +
+ +
+ + + + + +
+ +
+ + + +
+
+ + @if (level4Sub.get('criteriaType')?.value === 'group') { +
+ +
+ } + + @if (level4Sub.get('criteriaType')?.value === 'tag-based') { +
+ + +
+ + + + + } + + @if (level4Sub.get('criteriaType')?.value === 'criteria-based') { + + + + + } + + @if (level4Sub.get('criteriaType')?.value === 'group' && !level4Sub.get('isUniversal')?.value) { + + + + + } + + @if (level4Sub.get('criteriaType')?.value !== 'group' || !level4Sub.get('isUniversal')?.value) { + + + + + } + + +
+
Level 5 Subcriteria
+ + + @for (level5Sub of getSubcriteria($any(level4Sub)).controls; track $index; let l5Idx = $index) { +
+ +
+ Level 5 - {{ l5Idx + 1 }} + +
+ +
+ + + + + +
+ +
+ + + +
+
+ + @if (level5Sub.get('criteriaType')?.value === 'group') { +
+ +
+ } + + @if (level5Sub.get('criteriaType')?.value === 'tag-based') { +
+ + +
+ + + + + } + + @if (level5Sub.get('criteriaType')?.value === 'criteria-based') { + + + + + } + + @if (level5Sub.get('criteriaType')?.value === 'group' && !level5Sub.get('isUniversal')?.value) { + + + + + } + + @if (level5Sub.get('criteriaType')?.value !== 'group' || !level5Sub.get('isUniversal')?.value) { + + + + + } + +

Note: This is the deepest nesting level (Level 5). Further nesting is not supported.

+
+
+
+ } +
+
+
+
+ } +
+
+
+
+ } +
+
+
+
+ } +
+
+ } +
+ + +
+ + +
+
+ } @else { + +
+ + @if (budget.taggingCriteria.length > 0) { +
+

Tagging Criteria

+ @for (criterion of budget.taggingCriteria; track $index) { +
+
+ Tag: + {{ criterion.tag }} +
+
+ Condition: + {{ criterion.condition }} +
+
+ } +
+ } + + + @if (budget.transferCriteria.length > 0) { +
+

Transfer Criteria

+ @for (criterion of budget.transferCriteria; track $index) { +
+
+ Accuracy: + {{ criterion.accuracy }} +
+
+ Comment: + {{ criterion.comment }} +
+
+ Criterion: + {{ criterion.criterion }} +
+
+ } +
+ } + + +
+

Logbook Criteria

+ @if (budget.logbookCriteria) { + + } +
+
+ } +
+
+ } @else { + +
Loading budget...
+
+ } +
+ + + +
+
+ Description: + {{ criteria.description || 'N/A' }} +
+ @if (criteria.subcriteria && criteria.subcriteria.length > 0) { +
+ Type: + Group +
+ @if (criteria.isUniversal) { +
+ Universal (matches all operations) +
+ } + } + @if (criteria.type) { +
+ Type: + {{ criteria.type }} +
+ } + @if (criteria.tags && criteria.tags.length > 0) { +
+ Tags: +
+ @for (tag of criteria.tags; track tag) { + {{ tag }} + } +
+
+ } + @if (criteria.criteria) { +
+ {{ (criteria.subcriteria && criteria.subcriteria.length > 0) ? 'Pre-filter:' : 'Criteria:' }} + {{ criteria.criteria }} +
+ } + @if (criteria.substitution) { +
+ Substitution: + {{ criteria.substitution }} +
+ } + + + @if (criteria.subcriteria && criteria.subcriteria.length > 0) { +
+

Subcriteria:

+ @for (sub of criteria.subcriteria; track $index) { + + } +
+ } +
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.less new file mode 100644 index 00000000..4291488f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.less @@ -0,0 +1,435 @@ +.budget-detail-container { + margin: 0 auto; + padding: 2rem; +} + +.budget-card { + margin-bottom: 2rem; +} + +.header-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.budget-info-section { + padding: 1.5rem 0; + border-bottom: 1px solid var(--tui-border-normal); + margin-bottom: 1.5rem; +} + +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.info-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + font-weight: 500; +} + +.info-value { + font-size: 0.875rem; + padding: 0.25rem 0.5rem; + background: var(--tui-background-neutral-1); + border-radius: 4px; + font-family: 'Courier New', monospace; +} + +.owners-section { + margin-top: 1.5rem; +} + +.owners-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.budget-form { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.form-section { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.section-title-with-toggle { + display: flex; + align-items: center; + gap: 0.75rem; + + button { + min-width: 5rem; + font-size: 0.8125rem; + } +} + +.criterion-fields { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; +} + +.form-actions { + display: flex; + gap: 1rem; + padding-top: 1.5rem; + border-top: 1px solid var(--tui-border-normal); +} + +.read-only-view { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.criteria-section { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.criterion-card { + background: var(--tui-background-neutral-1); + border: 1px solid var(--tui-border-normal); + border-radius: 8px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.criterion-row { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.criterion-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + font-weight: 500; +} + +.criterion-value { + font-size: 0.875rem; + padding: 0.5rem; + background: var(--tui-background-base); + border-radius: 4px; + word-break: break-all; + font-family: 'Courier New', monospace; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.loading-placeholder { + padding: 3rem; + text-align: center; + color: var(--tui-text-secondary); +} + +tui-accordion-item { + margin-bottom: 0.5rem; + + header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } +} + +.logbook-fields { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.checkbox-field { + padding: 0.75rem 0; + + label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: 0.9375rem; + + input[type="checkbox"] { + width: 1.25rem; + height: 1.25rem; + cursor: pointer; + } + } +} + +.radio-group { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.75rem 0; +} + +.radio-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + font-weight: 500; +} + +.radio-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-left: 0.5rem; +} + +.radio-option { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: 0.9375rem; + padding: 0.5rem; + border-radius: 4px; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--tui-background-neutral-1); + } + + input[type="radio"] { + width: 1.125rem; + height: 1.125rem; + cursor: pointer; + } +} + +.select-field { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.select-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + font-weight: 500; +} + +.tui-select { + width: 100%; + padding: 0.75rem; + font-size: 0.9375rem; + border: 1px solid var(--tui-border-normal); + border-radius: 8px; + background-color: var(--tui-background-base); + color: var(--tui-text-primary); + cursor: pointer; + transition: border-color 0.2s ease; + + &:hover { + border-color: var(--tui-border-hover); + } + + &:focus { + outline: none; + border-color: var(--tui-border-focus); + box-shadow: 0 0 0 3px var(--tui-background-accent-1); + } + + option { + padding: 0.5rem; + } +} + +.subcriteria-section { + margin-top: 1.5rem; + padding: 1rem; + border: 2px dashed var(--tui-border-normal); + border-radius: 8px; + background: var(--tui-background-neutral-1-hover); + + h4 { + margin-bottom: 1rem; + } + + button { + margin-bottom: 1rem; + } +} + +.subcriterion-card { + margin-top: 1rem; + padding: 1rem; + border: 1px solid var(--tui-border-normal); + border-radius: 8px; + background: var(--tui-background-base); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.subcriterion-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--tui-border-normal); + + span { + font-weight: 600; + color: var(--tui-text-primary); + } +} + +.subcriteria-fields { + padding-left: 1rem; + border-left: 3px solid var(--tui-primary); +} + +.nested-subcriteria { + margin-top: 1rem; + padding: 1rem; + border: 1px dashed var(--tui-border-normal); + border-radius: 6px; + background: var(--tui-background-neutral-2); + + h5 { + font-size: 0.875rem; + margin-bottom: 0.75rem; + color: var(--tui-text-secondary); + } + + button { + margin-bottom: 0.75rem; + } +} + +.nested-subcriterion-card { + margin-top: 0.75rem; + padding: 0.75rem; + border: 1px solid var(--tui-border-normal); + border-radius: 6px; + background: var(--tui-background-base); +} + +.nested-fields { + padding-left: 0.75rem; + border-left: 2px solid var(--tui-secondary); +} + +.level4-subcriteria { + margin-top: 1rem; + padding: 0.75rem; + border: 1px dashed var(--tui-border-normal); + border-radius: 6px; + background: var(--tui-background-neutral-1); + + h6 { + font-size: 0.8125rem; + margin-bottom: 0.5rem; + color: var(--tui-text-secondary); + font-weight: 600; + } + + button { + margin-bottom: 0.5rem; + } +} + +.level4-subcriterion-card { + margin-top: 0.5rem; + padding: 0.625rem; + border: 1px solid var(--tui-border-normal); + border-radius: 6px; + background: var(--tui-background-base); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); +} + +.level4-fields { + padding-left: 0.625rem; + border-left: 2px solid var(--tui-accent); +} + +.level5-subcriteria { + margin-top: 0.75rem; + padding: 0.5rem; + border: 1px dashed var(--tui-border-normal); + border-radius: 4px; + background: var(--tui-background-neutral-2); + + h6 { + font-size: 0.75rem; + margin-bottom: 0.5rem; + color: var(--tui-text-tertiary); + font-weight: 600; + } + + button { + margin-bottom: 0.5rem; + } +} + +.level5-subcriterion-card { + margin-top: 0.5rem; + padding: 0.5rem; + border: 1px solid var(--tui-border-normal); + border-radius: 4px; + background: var(--tui-background-base); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02); +} + +.level5-fields { + padding-left: 0.5rem; + border-left: 2px solid var(--tui-info); +} + +.nesting-note { + margin-top: 0.5rem; + font-size: 0.8125rem; + color: var(--tui-text-tertiary); + font-style: italic; +} + +.subcriteria-display { + margin-top: 1rem; + padding: 1rem; + border-left: 3px solid var(--tui-primary); + background: var(--tui-background-neutral-1-hover); + border-radius: 4px; +} + +.subcriteria-title { + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.75rem; + color: var(--tui-text-secondary); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.spec.ts new file mode 100644 index 00000000..2c20ed5e --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BudgetDetailComponent } from './budget-detail.component'; + +describe('BudgetDetailComponent', () => { + let component: BudgetDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BudgetDetailComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BudgetDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.ts new file mode 100644 index 00000000..081b1e97 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-detail/budget-detail.component.ts @@ -0,0 +1,404 @@ +import { AsyncPipe, CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, FormArray } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, map, switchMap, catchError, of, tap } from 'rxjs'; +import { BudgetApiService } from '../budget-api.service'; +import { BudgetResponse, UpdateBudgetRequest, Owner } from '../models'; +import { + TuiButton, + TuiDialogService, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel +} from '@taiga-ui/core'; +import { + TuiAccordion, + TuiChip, + TuiTextarea +} from '@taiga-ui/kit'; +import { TuiCardLarge } from '@taiga-ui/layout'; + +@Component({ + selector: 'app-budget-detail', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TuiButton, + TuiLoader, + TuiTextfield, + TuiLabel, + TuiCardLarge, + TuiAccordion, + TuiChip, + TuiTextarea, + TuiTitle + ], + templateUrl: './budget-detail.component.html', + styleUrls: ['./budget-detail.component.less'] +}) +export class BudgetDetailComponent implements OnInit { + budgetId$!: Observable; + budget$!: Observable; + budget: BudgetResponse | null = null; + + budgetForm!: FormGroup; + isEditMode = false; + isLoading = false; + + // Section visibility toggles + showTaggingCriteria = true; + showTransferCriteria = true; + showLogbookCriteria = true; + + // LogbookCriteria type options + readonly tagBasedCriterionTypes = ['Including', 'Excluding', 'OneOf']; + + constructor( + private route: ActivatedRoute, + private router: Router, + private apiService: BudgetApiService, + private fb: FormBuilder, + private dialogService: TuiDialogService + ) {} + + ngOnInit(): void { + this.budgetId$ = this.route.params.pipe(map(params => params['budgetId'])); + + this.budget$ = this.budgetId$.pipe( + switchMap(id => this.apiService.getBudgetById(id).pipe( + tap(budget => { + this.budget = budget || null; + this.initForm(); + }), + catchError(error => { + console.error('Error fetching budget:', error); + this.showError('Failed to load budget details'); + return of(undefined); + }) + )) + ); + } + + initForm(): void { + if (!this.budget) return; + + this.budgetForm = this.fb.group({ + name: [this.budget.name, Validators.required], + version: [this.budget.version], + taggingCriteria: this.fb.array( + this.budget.taggingCriteria.map(tc => this.fb.group({ + tag: [tc.tag, Validators.required], + condition: [tc.condition, Validators.required] + })) + ), + transferCriteria: this.fb.array( + this.budget.transferCriteria.map(tc => this.fb.group({ + accuracy: [tc.accuracy, Validators.required], + comment: [tc.comment, Validators.required], + criterion: [tc.criterion, Validators.required] + })) + ), + logbookCriteria: this.createLogbookCriteriaGroup(this.budget.logbookCriteria) + }); + } + + createLogbookCriteriaGroup(criteria: any): FormGroup { + // Determine criteria type + let criteriaType = 'group'; + let isUniversal = false; + + if (criteria.type && criteria.tags) { + criteriaType = 'tag-based'; + } else if (criteria.criteria && (!criteria.subcriteria || criteria.subcriteria.length === 0)) { + // It's criteria-based only if it has criteria expression but no subcriteria + criteriaType = 'criteria-based'; + } else { + // It's a group (has subcriteria or is universal) + criteriaType = 'group'; + isUniversal = criteria.isUniversal || false; + } + + const group = this.fb.group({ + criteriaType: [criteriaType], + description: [criteria.description || '', Validators.required], + isUniversal: [isUniversal], + type: [criteria.type || ''], + tags: [criteria.tags ? criteria.tags.join(', ') : ''], + substitution: [criteria.substitution || ''], + criteria: [criteria.criteria || ''], + subcriteria: this.fb.array( + criteria.subcriteria?.map((sub: any) => this.createLogbookCriteriaGroup(sub)) || [] + ) + }); + + return group; + } + + getSubcriteria(criteriaGroup: FormGroup): FormArray { + return criteriaGroup.get('subcriteria') as FormArray; + } + + addSubcriterion(criteriaGroup: FormGroup): void { + const subcriteria = this.getSubcriteria(criteriaGroup); + subcriteria.push(this.createLogbookCriteriaGroup({ + description: '', + subcriteria: [] + })); + } + + removeSubcriterion(criteriaGroup: FormGroup, index: number): void { + const subcriteria = this.getSubcriteria(criteriaGroup); + subcriteria.removeAt(index); + } + + get taggingCriteria(): FormArray { + return this.budgetForm?.get('taggingCriteria') as FormArray; + } + + get transferCriteria(): FormArray { + return this.budgetForm?.get('transferCriteria') as FormArray; + } + + get logbookCriteria(): FormGroup { + return this.budgetForm?.get('logbookCriteria') as FormGroup; + } + + toggleEditMode(): void { + this.isEditMode = !this.isEditMode; + if (!this.isEditMode) { + this.initForm(); + } + } + + addTaggingCriterion(): void { + this.taggingCriteria.push(this.fb.group({ + tag: ['', Validators.required], + condition: ['', Validators.required] + })); + } + + removeTaggingCriterion(index: number): void { + this.taggingCriteria.removeAt(index); + } + + addTransferCriterion(): void { + this.transferCriteria.push(this.fb.group({ + accuracy: ['Exact', Validators.required], + comment: ['', Validators.required], + criterion: ['', Validators.required] + })); + } + + removeTransferCriterion(index: number): void { + this.transferCriteria.removeAt(index); + } + + toggleTaggingCriteria(): void { + this.showTaggingCriteria = !this.showTaggingCriteria; + } + + toggleTransferCriteria(): void { + this.showTransferCriteria = !this.showTransferCriteria; + } + + toggleLogbookCriteria(): void { + this.showLogbookCriteria = !this.showLogbookCriteria; + } + + buildLogbookCriteriaFromForm(formGroup: FormGroup): any { + const criteriaType = formGroup.get('criteriaType')?.value; + const description = formGroup.get('description')?.value; + const substitution = formGroup.get('substitution')?.value; + const subcriteriaArray = formGroup.get('subcriteria') as FormArray; + const isUniversal = formGroup.get('isUniversal')?.value; + + const baseCriteria: any = { + description, + substitution: substitution || undefined + }; + + if (criteriaType === 'group') { + // Group type - may have isUniversal flag and/or pre-filter criteria + if (isUniversal) { + baseCriteria.isUniversal = true; + } + + // Add pre-filter criteria if specified and not universal + const criteriaExpr = formGroup.get('criteria')?.value; + if (criteriaExpr && !isUniversal) { + baseCriteria.criteria = criteriaExpr; + } + } else if (criteriaType === 'tag-based') { + const tags = formGroup.get('tags')?.value; + baseCriteria.type = formGroup.get('type')?.value; + baseCriteria.tags = tags ? tags.split(',').map((t: string) => t.trim()).filter((t: string) => t) : undefined; + } else if (criteriaType === 'criteria-based') { + baseCriteria.criteria = formGroup.get('criteria')?.value; + } + + // Recursively build subcriteria + if (subcriteriaArray && subcriteriaArray.length > 0) { + baseCriteria.subcriteria = subcriteriaArray.controls.map(ctrl => + this.buildLogbookCriteriaFromForm(ctrl as FormGroup) + ); + } + + return baseCriteria; + } + + saveBudget(): void { + if (!this.budgetForm.valid || !this.budget) return; + + this.isLoading = true; + const formValue = this.budgetForm.value; + + const logbookCriteria = this.buildLogbookCriteriaFromForm(this.logbookCriteria); + + const request: UpdateBudgetRequest = { + name: formValue.name, + version: this.budget.version, + taggingCriteria: formValue.taggingCriteria, + transferCriteria: formValue.transferCriteria, + logbookCriteria: logbookCriteria + }; + + this.apiService.updateBudget(this.budget.id, request).subscribe({ + next: () => { + this.isLoading = false; + this.isEditMode = false; + this.showSuccess('Budget updated successfully'); + window.location.reload(); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to update budget'); + } + }); + } + + deleteBudget(): void { + if (!this.budget) return; + + const confirmed = confirm('Are you sure you want to delete this budget? This action cannot be undone.'); + if (confirmed && this.budget) { + this.isLoading = true; + this.apiService.removeBudget(this.budget.id, this.budget.version).subscribe({ + next: () => { + this.isLoading = false; + this.showSuccess('Budget deleted successfully'); + this.router.navigate(['/']); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to delete budget'); + } + }); + } + } + + navigateToReadingSettings(): void { + if (this.budget) { + this.router.navigate(['/budget', this.budget.id, 'reading-settings']); + } + } + + navigateToOperations(): void { + if (this.budget) { + this.router.navigate(['/budget', this.budget.id, 'operations']); + } + } + + downloadYaml(): void { + if (!this.budget) return; + + this.apiService.downloadBudgetYaml(this.budget.id).subscribe({ + next: (blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `budget-${this.budget!.name}.yaml`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }, + error: (error) => { + this.handleError(error, 'Failed to download YAML'); + } + }); + } + + uploadYaml(): void { + if (!this.budget) return; + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.yaml,.yml'; + input.onchange = (event: any) => { + const file = event.target?.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const yamlContent = e.target?.result as string; + if (!yamlContent) { + this.showError('Failed to read file content'); + return; + } + + this.isLoading = true; + this.apiService.uploadBudgetYaml(this.budget!.id, yamlContent).subscribe({ + next: () => { + this.isLoading = false; + this.showSuccess('Budget updated successfully from YAML'); + window.location.reload(); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to upload YAML'); + } + }); + }; + reader.onerror = () => { + this.showError('Failed to read file'); + }; + reader.readAsText(file); + }; + input.click(); + } + + private handleError(error: any, defaultMessage: string): void { + let errorMessage = defaultMessage; + + if (error.status === 400 && Array.isArray(error.error)) { + const errors = error.error as any[]; + errorMessage = errors.map(e => e.message || e).join('; '); + } else if (error.error?.message) { + errorMessage = error.error.message; + } + + this.showError(errorMessage); + } + + private showError(message: string): void { + this.dialogService.open(message, { + label: 'Error', + size: 'm', + closeable: true, + dismissible: true + }).subscribe(); + } + + private showSuccess(message: string): void { + this.dialogService.open(message, { + label: 'Success', + size: 's', + closeable: true, + dismissible: true + }).subscribe(); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.html new file mode 100644 index 00000000..76923c1f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.html @@ -0,0 +1,22 @@ + + + + + + + {{budget.name}} + + + + + Add new budget + + + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.less new file mode 100644 index 00000000..d9ae495f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.less @@ -0,0 +1,5 @@ +:host { + display: flex; + gap: 8px; + align-items: center; +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.spec.ts new file mode 100644 index 00000000..ce2ae0f0 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BudgetSelectorComponent } from './budget-selector.component'; + +describe('BudgetSelectorComponent', () => { + let component: BudgetSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BudgetSelectorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BudgetSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.ts new file mode 100644 index 00000000..f6d38023 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/budget-selector/budget-selector.component.ts @@ -0,0 +1,66 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { BudgetApiService as BudgetApiService } from '../budget-api.service'; +import { BudgetResponse } from '../models'; +import { NavigationEnd, Router, RouterLink } from '@angular/router'; +import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'; +import { TuiChevron } from '@taiga-ui/kit' +import { BehaviorSubject, filter, Observable, Subscription } from 'rxjs'; +import { AsyncPipe, CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-budget-selector', + templateUrl: './budget-selector.component.html', + styleUrls: ['./budget-selector.component.less'], + imports: [CommonModule, TuiButton, TuiChevron, TuiDataList, TuiDropdown, RouterLink, AsyncPipe] +}) +export class BudgetSelectorComponent implements OnInit, OnDestroy { + private budgetIdPattern = new RegExp("^/budget/([^/]+)"); + private budgetSub: Subscription | undefined; + private routerSub: Subscription | undefined; + private selectedBudgetId: string | null = null; + + budgets$: Observable | undefined; + budgetsSnapshot: BudgetResponse[] = []; + selectedBudget$: BehaviorSubject = new BehaviorSubject(undefined); + + constructor( + private budgetApiService: BudgetApiService, + private router: Router + ) {} + + ngOnDestroy(): void { + console.log('destroying budget selector component'); + this.budgetSub?.unsubscribe(); + this.routerSub?.unsubscribe(); + } + + ngOnInit(): void { + this.setBudgetIdFrom(this.router.url); + + this.budgets$ = this.budgetApiService.getAllBudgets(); + this.routerSub = this.router.events + .pipe(filter(e => e instanceof NavigationEnd)) + .subscribe(n => { + this.setBudgetIdFrom(n.url); + this.updateSelectedBudget(); + }); + + this.budgetSub = this.budgets$.subscribe(budgets => { + this.budgetsSnapshot = budgets; + this.updateSelectedBudget(); + }); + } + + setBudgetIdFrom(url:string) { + this.selectedBudgetId = this.budgetIdPattern.exec(url)?.[1] ?? null; + } + + updateSelectedBudget() { + if (this.selectedBudgetId) { + const selectedBudget = this.budgetsSnapshot.find(budget => budget.id === this.selectedBudgetId); + this.selectedBudget$.next(selectedBudget); + } else { + this.selectedBudget$.next(undefined); + } + } +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/models.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/models.ts new file mode 100644 index 00000000..9a87d40d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/models.ts @@ -0,0 +1,218 @@ +// Budget models matching C# API +export interface Owner { + id: string; + name: string; +} + +export interface TaggingCriterionResponse { + tag: string; + condition: string; +} + +export interface TransferCriterionResponse { + accuracy: string; + comment: string; + criterion: string; +} + +export interface LogbookCriteriaResponse { + description: string; + subcriteria?: LogbookCriteriaResponse[]; + type?: string; + tags?: string[]; + substitution?: string; + criteria?: string; + isUniversal?: boolean; +} + +export interface BudgetResponse { + id: string; + name: string; + version: string; + owners: Owner[]; + taggingCriteria: TaggingCriterionResponse[]; + transferCriteria: TransferCriterionResponse[]; + logbookCriteria: LogbookCriteriaResponse; +} + +export interface BudgetIdentifier { + id: string; + version: string; +} + +export interface RegisterBudgetRequest { + name: string; +} + +export interface ChangeBudgetOwnersRequest { + budget: BudgetIdentifier; + ownerIds: string[]; +} + +export interface UpdateBudgetRequest { + name: string; + version: string; + taggingCriteria?: TaggingCriterionResponse[]; + transferCriteria?: TransferCriterionResponse[]; + logbookCriteria?: LogbookCriteriaResponse; +} + +export interface MergeBudgetsRequest { + budgetIds: string[]; + purgeEmptyBudgets: boolean; +} + +// File Reading Settings models +export interface ValidationRuleResponse { + pattern: string; + condition: string; + value: string; + errorMessage: string; +} + +export interface FileReadingSettingResponse { + culture: string; + encoding: string; + dateTimeKind: string; + fields: Record; + attributes: Record; + validation: ValidationRuleResponse[]; +} + +// Operation models +export interface MoneyResponse { + value: number; + currencyCode: string; +} + +export interface OperationResponse { + id: string; + version: string; + timestamp: string; + amount: MoneyResponse; + description: string; + budgetId: string; + tags: string[]; + attributes?: Record; +} + +export interface UpdateOperationRequest { + id: string; + version: string; + timestamp: string; + amount: MoneyResponse; + description: string; + tags: string[]; + attributes?: Record; +} + +export interface UpdateOperationsRequest { + budgetVersion: string; + operations: UpdateOperationRequest[]; + transferConfidenceLevel?: string; + taggingMode: string; +} + +export interface RemoveOperationsRequest { + criteria: string; +} + +export interface RetagOperationsRequest { + budgetVersion: string; + criteria: string; + fromScratch: boolean; +} + +export interface IReason { + message?: string; + metadata?: Record; + reasons?: IReason[]; +} + +export interface IError extends IReason {} + +export interface ISuccess extends IReason {} + +export interface ImportResultResponse { + registeredOperations: OperationResponse[]; + duplicates: OperationResponse[][]; + errors: IError[]; + successes: ISuccess[]; +} + +export interface UpdateResultResponse { + updatedOperations: OperationResponse[]; + errors: IError[]; + successes: ISuccess[]; +} + +export interface DeleteResultResponse { + errors: IError[]; + successes: ISuccess[]; +} + +export interface RetagResultResponse { + errors: IError[]; + successes: ISuccess[]; +} + +// Logbook models +export interface NamedRangeResponse { + name: string; + from: string; + till: string; +} + +export interface LogbookEntryResponse { + description: string; + sum: MoneyResponse; + from: string; + till: string; + operationsCount: number; + operations: OperationResponse[]; + children: LogbookEntryResponse[]; +} + +export interface RangedLogbookEntryResponse { + range: NamedRangeResponse; + entry: LogbookEntryResponse; +} + +export interface LogbookResponse { + ranges: RangedLogbookEntryResponse[]; + errors: IError[]; + successes: ISuccess[]; +} + +// Transfer models +export interface TransferResponse { + sourceId: string; + source: OperationResponse; + sinkId: string; + sink: OperationResponse; + fee: MoneyResponse; + comment: string; + accuracy: string; +} + +export interface RegisterTransferRequest { + sourceId: string; + sinkId: string; + fee?: MoneyResponse; + comment: string; + accuracy: string; +} + +export interface RegisterTransfersRequest { + transfers: RegisterTransferRequest[]; +} + +export interface RemoveTransfersRequest { + sourceIds: string[]; + all: boolean; +} + +export interface TransfersListResponse { + recorded: TransferResponse[]; + unregistered: TransferResponse[]; +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.html new file mode 100644 index 00000000..2ce0436a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.html @@ -0,0 +1,38 @@ +
+ + {{ errorMessage }} + +

+ Create New Budget +

+ + + + + + + \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.less new file mode 100644 index 00000000..e69de29b diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.spec.ts new file mode 100644 index 00000000..1a2cd1b6 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NewBudgetComponent } from './new-budget.component'; + +describe('NewBudgetComponent', () => { + let component: NewBudgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NewBudgetComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NewBudgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.ts new file mode 100644 index 00000000..3c0dea45 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/new-budget/new-budget.component.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { BudgetApiService } from '../budget-api.service'; +import { RegisterBudgetRequest } from '../models'; +import { CommonModule } from '@angular/common'; +import { TuiButton, TuiError, TuiNotification, TuiTextfield } from '@taiga-ui/core'; +import { TuiFieldErrorPipe, tuiValidationErrorsProvider } from '@taiga-ui/kit'; +import { TuiForm } from '@taiga-ui/layout'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-new-budget', + templateUrl: './new-budget.component.html', + styleUrls: ['./new-budget.component.less'], + imports: [FormsModule, ReactiveFormsModule, CommonModule, TuiNotification, TuiTextfield, TuiButton, TuiError, TuiFieldErrorPipe, TuiForm], + providers: [tuiValidationErrorsProvider({required: 'Please enter budget name'})] +}) +export class NewBudgetComponent { + nameGroup = new FormGroup({ + name: new FormControl('', [Validators.required]), + }); + + errorMessage: string | null = null; + + constructor(private budgetService: BudgetApiService, private router: Router) {} + + onSubmit() { + if (!this.nameGroup.controls.name.valid) { + this.errorMessage = 'Please enter budget name.'; + return; + } + + const request: RegisterBudgetRequest = { + name: this.nameGroup.controls.name.value ?? '', + }; + + this.budgetService.createBudget(request).subscribe({ + next: (response) => { + this.resetForm(); + this.router.navigate(['/budget', response.id]); + }, + error: (error) => { + this.handleError(error); + } + }); + } + + resetForm() { + this.nameGroup.controls.name.setValue(''); + this.errorMessage = null; + } + + handleError(error: any) { + if (error.status === 400 && Array.isArray(error.error)) { + this.errorMessage = error.error.map((err: any) => err.message).join(', '); + } else { + this.errorMessage = 'Error creating budget. Please try again.'; + } + } +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.html new file mode 100644 index 00000000..ba22c636 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.html @@ -0,0 +1,418 @@ +
+ +
+ +
+

File Reading Settings

+
+ + @if (!isEditMode) { + + + + } +
+
+ + + @if (isAddingNew && settingsForm) { +
+

Add New Pattern

+
+
+ + + + +
+ +
+
+ + + + +
+ +
+ + + + +
+ +
+ + +
+
+ + +
+
+

Fields

+ +
+
+ @for (field of fields.controls; track $index; let i = $index) { +
+ + + + + + + + + +
+ } +
+
+ + +
+
+

Attributes

+ +
+
+ @for (attr of attributes.controls; track $index; let i = $index) { +
+ + + + + + + + + +
+ } +
+
+ + +
+
+

Validation Rules

+ +
+
+ @for (rule of validationRules.controls; track $index; let i = $index) { +
+
+ + + + +
+ + +
+ + + + +
+
+ + + + + +
+
+ } +
+
+ +
+ + +
+
+
+ } + + + @if (!isAddingNew) { +
+ @if (getPatterns().length === 0) { +

No file reading patterns configured. Click "Add New Pattern" to create one.

+ } + + @for (pattern of getPatterns(); track pattern) { +
+ + +
+ {{ pattern }} +
+ + + @if (editingPattern !== pattern) { +
+
+
+ Culture: + {{ settings[pattern].culture }} +
+
+ Encoding: + {{ settings[pattern].encoding }} +
+
+ DateTime Kind: + {{ settings[pattern].dateTimeKind }} +
+ + @if (Object.keys(settings[pattern].fields).length > 0) { +
+
Fields
+
    + @for (field of Object.entries(settings[pattern].fields); track field[0]) { +
  • {{ field[0] }}: {{ field[1] }}
  • + } +
+
+ } + + @if (Object.keys(settings[pattern].attributes).length > 0) { +
+
Attributes
+
    + @for (attr of Object.entries(settings[pattern].attributes); track attr[0]) { +
  • {{ attr[0] }}: {{ attr[1] }}
  • + } +
+
+ } + + @if (settings[pattern].validation.length > 0) { +
+
Validation Rules
+
    + @for (rule of settings[pattern].validation; track $index) { +
  • + {{ rule.pattern }} {{ rule.condition }} {{ rule.value }} +
    Error: {{ rule.errorMessage }} +
  • + } +
+
+ } +
+ +
+ + +
+
+ } + + + @if (editingPattern === pattern && settingsForm) { +
+
+
+ + + + +
+ +
+
+ + + + +
+ +
+ + + + +
+ +
+ + +
+
+ + +
+
+

Fields

+ +
+
+ @for (field of fields.controls; track $index; let i = $index) { +
+ + + + + + + + + +
+ } +
+
+ + +
+
+

Attributes

+ +
+
+ @for (attr of attributes.controls; track $index; let i = $index) { +
+ + + + + + + + + +
+ } +
+
+ + +
+
+

Validation Rules

+ +
+
+ @for (rule of validationRules.controls; track $index; let i = $index) { +
+
+ + + + +
+ + +
+ + + + +
+
+ + + + + +
+
+ } +
+
+ +
+ + +
+
+
+ } +
+
+
+ } +
+ } +
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.less new file mode 100644 index 00000000..a2026aec --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.less @@ -0,0 +1,326 @@ +.reading-settings-container { + padding: 2rem; + margin: 0 auto; +} + +.settings-card { + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; + + h2 { + margin: 0; + } +} + +.header-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--tui-text-secondary); + font-style: italic; +} + +.patterns-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.pattern-item { + border: 1px solid var(--tui-border-normal); + border-radius: 8px; + overflow: hidden; +} + +.pattern-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: var(--tui-background-neutral-1); +} + +.pattern-code { + font-family: monospace; + font-size: 0.95rem; + background: var(--tui-background-base); + padding: 0.25rem 0.5rem; + border-radius: 4px; + word-break: break-all; +} + +.pattern-content { + padding: 1.5rem; +} + +.pattern-details { + margin-bottom: 1rem; +} + +.detail-row { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.detail-label { + font-weight: 600; + min-width: 120px; +} + +.detail-value { + color: var(--tui-text-secondary); +} + +.detail-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--tui-border-normal); + + h5 { + margin-bottom: 0.5rem; + } +} + +.detail-list { + list-style: none; + padding: 0; + margin: 0; + + li { + padding: 0.25rem 0; + + strong { + color: var(--tui-text-primary); + } + + code { + background: var(--tui-background-neutral-1); + padding: 0.125rem 0.25rem; + border-radius: 3px; + font-size: 0.9rem; + } + + em { + color: var(--tui-text-tertiary); + font-size: 0.9rem; + } + } +} + +.pattern-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--tui-border-normal); +} + +.edit-section { + background: var(--tui-background-base); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1.5rem; +} + +.settings-form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; +} + +.form-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; + + .form-group { + min-width: 200px; + } +} + +.subsection { + border: 1px solid var(--tui-border-normal); + border-radius: 6px; + padding: 1rem; + background: var(--tui-background-neutral-1); +} + +.subsection-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + + h4 { + margin: 0; + } +} + +.key-value-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.key-value-item { + display: flex; + gap: 0.75rem; + align-items: flex-end; + flex-wrap: wrap; + + .key-input { + flex: 1; + min-width: 150px; + } + + .value-input { + flex: 2; + min-width: 200px; + } +} + +.validation-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.validation-item { + border: 1px solid var(--tui-border-normal); + padding: 1rem; + border-radius: 4px; + background: var(--tui-background-base); +} + +.validation-row { + display: flex; + gap: 0.75rem; + align-items: flex-end; + flex-wrap: wrap; + margin-bottom: 0.75rem; + + &:last-child { + margin-bottom: 0; + } + + .pattern-input { + flex: 2; + min-width: 150px; + } + + .condition-input { + flex: 1; + min-width: 120px; + } + + .value-input { + flex: 1; + min-width: 120px; + } + + .error-message-input { + flex: 3; + min-width: 250px; + } +} + +.form-actions { + display: flex; + gap: 0.75rem; + padding-top: 1rem; + border-top: 1px solid var(--tui-border-normal); + flex-wrap: wrap; +} + +.select-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 1; +} + +.select-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + font-weight: 500; +} + +.tui-select { + width: 100%; + padding: 0.75rem; + font-size: 0.9375rem; + border: 1px solid var(--tui-border-normal); + border-radius: 8px; + background-color: var(--tui-background-base); + color: var(--tui-text-primary); + cursor: pointer; + transition: border-color 0.2s ease; + + &:hover { + border-color: var(--tui-border-hover); + } + + &:focus { + outline: none; + border-color: var(--tui-border-focus); + box-shadow: 0 0 0 3px var(--tui-background-accent-1); + } + + option { + padding: 0.5rem; + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .reading-settings-container { + padding: 1rem; + } + + .settings-card { + padding: 1rem; + } + + .header { + flex-direction: column; + align-items: flex-start; + } + + .form-row, + .key-value-item, + .validation-row { + flex-direction: column; + align-items: stretch; + + .form-group, + .key-input, + .value-input, + .pattern-input, + .condition-input, + .error-message-input { + min-width: 100%; + } + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.spec.ts new file mode 100644 index 00000000..ebad5108 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReadingSettingsComponent } from './reading-settings.component'; +import { ActivatedRoute } from '@angular/router'; +import { BudgetApiService } from '../budget-api.service'; +import { of } from 'rxjs'; + +describe('ReadingSettingsComponent', () => { + let component: ReadingSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + const apiServiceMock = { + getReadingSettings: jasmine.createSpy('getReadingSettings').and.returnValue(of({ settings: {} })), + updateReadingSettings: jasmine.createSpy('updateReadingSettings').and.returnValue(of(void 0)) + }; + + const activatedRouteMock = { + params: of({ budgetId: 'test-id' }) + }; + + await TestBed.configureTestingModule({ + imports: [ReadingSettingsComponent], + providers: [ + { provide: BudgetApiService, useValue: apiServiceMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ReadingSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.ts new file mode 100644 index 00000000..3e2ec00b --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/budget/reading-settings/reading-settings.component.ts @@ -0,0 +1,367 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, FormArray } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, map } from 'rxjs'; +import { BudgetApiService } from '../budget-api.service'; +import { FileReadingSettingResponse, ValidationRuleResponse } from '../models'; +import { + TuiButton, + TuiDialogService, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel, + TuiDataList +} from '@taiga-ui/core'; +import { + TuiAccordion +} from '@taiga-ui/kit'; +import { TuiCardLarge } from '@taiga-ui/layout'; + +@Component({ + selector: 'app-reading-settings', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TuiButton, + TuiLoader, + TuiTextfield, + TuiLabel, + TuiCardLarge, + TuiAccordion, + TuiTitle, + TuiDataList + ], + templateUrl: './reading-settings.component.html', + styleUrls: ['./reading-settings.component.less'] +}) +export class ReadingSettingsComponent implements OnInit { + budgetId$!: Observable; + budgetId: string = ''; + settings: Record = {}; + + settingsForm!: FormGroup; + isEditMode = false; + isLoading = false; + editingPattern: string | null = null; + isAddingNew = false; + + readonly dateTimeKindOptions = ['Local', 'Utc', 'Unspecified']; + readonly validationConditionOptions = ['Equals', 'NotEquals']; + + // Make Object available in template + readonly Object = Object; + + constructor( + private route: ActivatedRoute, + private router: Router, + private apiService: BudgetApiService, + private fb: FormBuilder, + private dialogService: TuiDialogService + ) {} + + ngOnInit(): void { + this.budgetId$ = this.route.params.pipe(map(params => params['budgetId'])); + + this.budgetId$.subscribe(id => { + this.budgetId = id; + this.loadSettings(); + }); + } + + loadSettings(): void { + this.isLoading = true; + this.apiService.getReadingSettings(this.budgetId).subscribe({ + next: (response) => { + this.settings = response || {}; + this.isLoading = false; + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to load reading settings'); + } + }); + } + + getPatterns(): string[] { + return Object.keys(this.settings); + } + + startEdit(pattern: string): void { + this.editingPattern = pattern; + const setting = this.settings[pattern]; + this.settingsForm = this.createSettingForm(pattern, setting); + this.isEditMode = true; + } + + startAddNew(): void { + this.isAddingNew = true; + this.editingPattern = null; + this.settingsForm = this.createSettingForm('', { + culture: 'en-US', + encoding: 'utf-8', + dateTimeKind: 'Local', + fields: {}, + attributes: {}, + validation: [] + }); + this.isEditMode = true; + } + + createSettingForm(pattern: string, setting: FileReadingSettingResponse): FormGroup { + return this.fb.group({ + pattern: [pattern, Validators.required], + culture: [setting.culture, Validators.required], + encoding: [setting.encoding, Validators.required], + dateTimeKind: [setting.dateTimeKind, Validators.required], + fields: this.fb.array( + Object.entries(setting.fields).map(([key, value]) => + this.fb.group({ + key: [key, Validators.required], + value: [value, Validators.required] + }) + ) + ), + attributes: this.fb.array( + Object.entries(setting.attributes).map(([key, value]) => + this.fb.group({ + key: [key, Validators.required], + value: [value, Validators.required] + }) + ) + ), + validation: this.fb.array( + setting.validation.map(v => + this.fb.group({ + pattern: [v.pattern, Validators.required], + condition: [v.condition, Validators.required], + value: [v.value, Validators.required], + errorMessage: [v.errorMessage, Validators.required] + }) + ) + ) + }); + } + + get fields(): FormArray { + return this.settingsForm?.get('fields') as FormArray; + } + + get attributes(): FormArray { + return this.settingsForm?.get('attributes') as FormArray; + } + + get validationRules(): FormArray { + return this.settingsForm?.get('validation') as FormArray; + } + + addField(): void { + this.fields.push(this.fb.group({ + key: ['', Validators.required], + value: ['', Validators.required] + })); + } + + removeField(index: number): void { + this.fields.removeAt(index); + } + + addAttribute(): void { + this.attributes.push(this.fb.group({ + key: ['', Validators.required], + value: ['', Validators.required] + })); + } + + removeAttribute(index: number): void { + this.attributes.removeAt(index); + } + + addValidationRule(): void { + this.validationRules.push(this.fb.group({ + pattern: ['', Validators.required], + condition: ['Equals', Validators.required], + value: ['', Validators.required], + errorMessage: ['', Validators.required] + })); + } + + removeValidationRule(index: number): void { + this.validationRules.removeAt(index); + } + + saveSettings(): void { + if (!this.settingsForm.valid) return; + + this.isLoading = true; + const formValue = this.settingsForm.value; + + // Convert arrays to dictionaries + const fields: Record = {}; + formValue.fields.forEach((f: any) => { + fields[f.key] = f.value; + }); + + const attributes: Record = {}; + formValue.attributes.forEach((a: any) => { + attributes[a.key] = a.value; + }); + + const validation: ValidationRuleResponse[] = formValue.validation.map((v: any) => ({ + pattern: v.pattern, + condition: v.condition, + value: v.value, + errorMessage: v.errorMessage + })); + + const newSetting: FileReadingSettingResponse = { + culture: formValue.culture, + encoding: formValue.encoding, + dateTimeKind: formValue.dateTimeKind, + fields, + attributes, + validation + }; + + // Create updated settings + const updatedSettings = { ...this.settings }; + + // If editing and pattern changed, remove old pattern + if (this.editingPattern && this.editingPattern !== formValue.pattern) { + delete updatedSettings[this.editingPattern]; + } + + updatedSettings[formValue.pattern] = newSetting; + + this.apiService.updateReadingSettings(this.budgetId, updatedSettings).subscribe({ + next: () => { + this.isLoading = false; + this.isEditMode = false; + this.isAddingNew = false; + this.editingPattern = null; + this.showSuccess('Settings saved successfully'); + this.loadSettings(); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to save settings'); + } + }); + } + + deletePattern(pattern: string): void { + if (!confirm(`Are you sure you want to delete the pattern "${pattern}"?`)) { + return; + } + + this.isLoading = true; + const updatedSettings = { ...this.settings }; + delete updatedSettings[pattern]; + + this.apiService.updateReadingSettings(this.budgetId, updatedSettings).subscribe({ + next: () => { + this.isLoading = false; + this.showSuccess('Pattern deleted successfully'); + this.loadSettings(); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to delete pattern'); + } + }); + } + + cancelEdit(): void { + this.isEditMode = false; + this.isAddingNew = false; + this.editingPattern = null; + this.settingsForm = null as any; + } + + goBack(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + downloadYaml(): void { + this.isLoading = true; + this.apiService.downloadReadingSettingsYaml(this.budgetId).subscribe({ + next: (blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `reading-settings-${this.budgetId}.yaml`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + this.isLoading = false; + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to download YAML'); + } + }); + } + + uploadYaml(): void { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.yaml,.yml'; + input.onchange = (event: any) => { + const file = event.target?.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const yamlContent = e.target?.result as string; + if (!yamlContent) { + this.handleError({}, 'Failed to read file content'); + return; + } + + this.isLoading = true; + this.apiService.uploadReadingSettingsYaml(this.budgetId, yamlContent).subscribe({ + next: () => { + this.isLoading = false; + this.showSuccess('Reading settings updated successfully from YAML'); + this.loadSettings(); + }, + error: (error) => { + this.isLoading = false; + this.handleError(error, 'Failed to upload YAML'); + } + }); + }; + reader.readAsText(file); + }; + input.click(); + } + + private showSuccess(message: string): void { + this.dialogService + .open(message, { label: 'Success', size: 's' }) + .subscribe(); + } + + private handleError(error: any, defaultMessage: string): void { + let errorMessage = defaultMessage; + + if (error?.error) { + if (Array.isArray(error.error)) { + const errors = error.error.map((e: any) => e.message || e).join(', '); + errorMessage = `${defaultMessage}: ${errors}`; + } else if (typeof error.error === 'string') { + errorMessage = `${defaultMessage}: ${error.error}`; + } else if (error.error.message) { + errorMessage = `${defaultMessage}: ${error.error.message}`; + } + } + + this.dialogService + .open(errorMessage, { label: 'Error', size: 'm' }) + .subscribe(); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.initializer.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.initializer.ts new file mode 100644 index 00000000..3a4b71dc --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.initializer.ts @@ -0,0 +1,19 @@ +import { inject, provideAppInitializer } from '@angular/core'; +import { AppConfigService } from './app-config.service'; +import { AuthService } from '../auth/auth/auth.service'; + +export function initializeApp( + configService: AppConfigService, + authService: AuthService +): Promise { + return configService.loadConfig().then(() => { + // Set the API URL in AuthService after config is loaded + authService.setBaseUrl(configService.apiUrl); + }); +} + +export const appConfigInitializerProvider = provideAppInitializer(() => { + const configService = inject(AppConfigService); + const authService = inject(AuthService); + return initializeApp(configService, authService); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.service.ts new file mode 100644 index 00000000..0bd8e0db --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/config/app-config.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +export interface AppConfig { + apiUrl: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class AppConfigService { + private config?: AppConfig; + + constructor(private http: HttpClient) {} + + loadConfig(): Promise { + return firstValueFrom( + this.http.get('/api/config') + ).then(config => { + this.config = config; + }).catch(error => { + console.error('Failed to load application configuration:', error); + // Fallback to default config + this.config = { + apiUrl: 'https://localhost:25001' + }; + }); + } + + get apiUrl(): string { + return this.config?.apiUrl || 'https://localhost:25001'; + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.html new file mode 100644 index 00000000..66900150 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.html @@ -0,0 +1,93 @@ +
+ @if (isAuthenticated$ | async) { +
+ + + @if (budgets$ | async; as budgets) { + @if (budgets.length > 0) { +
+ @for (budget of budgets; track budget.id) { +
+

{{ budget.name }}

+ +
+
+ Owners: +
+ @for (owner of budget.owners; track owner.id) { + + {{ owner.name }} + + } +
+
+ +
+ Tagging Rules: + + {{ budget.taggingCriteria.length }} + +
+ +
+ Transfer Rules: + + {{ budget.transferCriteria.length }} + +
+
+ +
+ +
+
+ } +
+ } @else { +
+

No budgets yet

+

Create your first budget to get started!

+ +
+ } + } @else { + +
Loading budgets...
+
+ } +
+ } @else { +
+

Welcome to Budget Manager

+

Please log in to manage your budgets.

+
+ } +
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.less new file mode 100644 index 00000000..59f57d58 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.less @@ -0,0 +1,99 @@ +.index-container { + margin: 0 auto; + padding: 2rem; +} + +.budgets-page { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.budgets-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 1.5rem; +} + +.budget-item { + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } +} + +.budget-summary { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin: 1rem 0; +} + +.summary-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.summary-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + min-width: 100px; +} + +.owners-chips { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.budget-actions { + display: flex; + gap: 0.5rem; + padding-top: 1rem; + border-top: 1px solid var(--tui-border-normal); +} + +.empty-state { + text-align: center; + padding: 3rem; + + h3 { + margin-bottom: 0.5rem; + } + + p { + color: var(--tui-text-secondary); + margin-bottom: 1.5rem; + } +} + +.welcome-card { + text-align: center; + padding: 3rem; + margin: 2rem auto; + + h2 { + margin-bottom: 1rem; + } + + p { + color: var(--tui-text-secondary); + } +} + +.loading-placeholder { + padding: 3rem; + text-align: center; + color: var(--tui-text-secondary); +} \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.spec.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.spec.ts new file mode 100644 index 00000000..322e0c8a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IndexComponent } from './index.component'; + +describe('IndexComponent', () => { + let component: IndexComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IndexComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(IndexComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.ts new file mode 100644 index 00000000..c5842e22 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/index/index.component.ts @@ -0,0 +1,79 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { TuiButton, TuiDialogService, TuiLoader, TuiTitle } from '@taiga-ui/core'; +import { UserService } from '../auth/user.service'; +import { BudgetApiService } from '../budget/budget-api.service'; +import { BudgetResponse } from '../budget/models'; +import { Observable, map, catchError, of } from 'rxjs'; +import { AsyncPipe, CommonModule } from '@angular/common'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { TuiChip } from '@taiga-ui/kit'; + +@Component({ + selector: 'app-index', + standalone: true, + imports: [ + CommonModule, + AsyncPipe, + TuiButton, + TuiCardLarge, + TuiChip, + TuiLoader, + TuiTitle + ], + templateUrl: './index.component.html', + styleUrl: './index.component.less' +}) +export class IndexComponent { + isAuthenticated$: Observable; + budgets$: Observable; + + constructor( + private user: UserService, + private budgetService: BudgetApiService, + private router: Router, + private dialogService: TuiDialogService + ) { + this.isAuthenticated$ = user.current$.pipe(map(u => u.isAuthenticated)); + this.budgets$ = this.budgetService.getAllBudgets().pipe( + catchError(error => { + console.error('Error loading budgets:', error); + return of([]); + }) + ); + } + + createNewBudget(): void { + this.router.navigate(['/budget/new']); + } + + viewBudget(budgetId: string): void { + this.router.navigate(['/budget', budgetId]); + } + + deleteBudget(budget: BudgetResponse, event: Event): void { + event.stopPropagation(); + + const confirmed = confirm(`Are you sure you want to delete budget "${budget.name}"?`); + if (confirmed) { + this.budgetService.removeBudget(budget.id, budget.version).subscribe({ + next: () => { + this.dialogService.open('Budget deleted successfully', { + label: 'Success', + size: 's' + }).subscribe(); + }, + error: (error) => { + let errorMessage = 'Failed to delete budget'; + if (error.status === 400 && Array.isArray(error.error)) { + errorMessage = error.error.map((err: any) => err.message).join('; '); + } + this.dialogService.open(errorMessage, { + label: 'Error', + size: 'm' + }).subscribe(); + } + }); + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.html new file mode 100644 index 00000000..197ce178 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.html @@ -0,0 +1,66 @@ +
+ +
+

Delete Operations

+ +
+

⚠️ Warning: Dangerous Operation

+

This action will permanently delete all operations matching your criteria. This cannot be undone.

+

Please be careful and verify your criteria before executing the deletion.

+
+ + + + +
+ + @if (deleteResult) { + + } +
+ + @if (deleteResult) { +
+

Deletion Results

+
+ @if (deleteResult.successes.length > 0) { +
+ Successes: + {{ deleteResult.successes.length }} +
+ } + @if (deleteResult.errors.length > 0) { +
+ Errors: + {{ deleteResult.errors.length }} +
+ } +
+ + + +
+ } +
+
+
diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.less new file mode 100644 index 00000000..b83c3927 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.less @@ -0,0 +1,45 @@ +@import '../shared/styles/mixins.less'; + +.delete-container { + .page-container(); +} + +.delete-card { + .card-base(); +} + +.danger-warning { + margin: 1.5rem 0; + padding: 1.5rem; + background: var(--tui-error-bg); + border: 2px solid var(--tui-error-fill); + border-radius: 0.5rem; + + h3 { + margin: 0 0 1rem 0; + color: var(--tui-error-fill); + font-size: 1.125rem; + } + + p { + margin: 0.5rem 0; + color: var(--tui-text-primary); + line-height: 1.5; + } +} + +.action-buttons { + display: flex; + gap: 1rem; + margin: 2rem 0; + flex-wrap: wrap; +} + +.delete-result { + .result-section-base(); +} + +.result-stats { + .result-stats-base(); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.ts new file mode 100644 index 00000000..cc120161 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/delete-operations/delete-operations.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { + TuiButton, + TuiLoader, + TuiTitle +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { NotificationService } from '../shared/notification.service'; +import { CriteriaFilterComponent } from '../shared/components/criteria-filter/criteria-filter.component'; +import { OperationResultComponent } from '../shared/components/operation-result/operation-result.component'; +import { CriteriaExample } from '../shared/models/example.interface'; +import { OperationResult } from '../shared/models/result.interface'; + +@Component({ + selector: 'app-delete-operations', + standalone: true, + imports: [ + CommonModule, + TuiButton, + TuiLoader, + TuiCardLarge, + TuiTitle, + CriteriaFilterComponent, + OperationResultComponent + ], + templateUrl: './delete-operations.component.html', + styleUrls: ['./delete-operations.component.less'] +}) +export class DeleteOperationsComponent implements OnInit { + budgetId!: string; + isLoading = false; + deleteResult: OperationResult | null = null; + currentCriteria = 'o => true'; + + criteriaExamples: CriteriaExample[] = [ + { label: 'All operations:', code: 'o => true' }, + { label: 'Negative amounts:', code: 'o => o.Amount.Amount < 0' }, + { label: 'Specific year:', code: 'o => o.Timestamp.Year == 2023' }, + { label: 'Contains text:', code: 'o => o.Description.Contains("test")' }, + { label: 'By tag:', code: 'o => o.Tags.Any(t => t.Value == "unwanted")' }, + { label: 'By attribute:', code: 'o => o.Attributes.ContainsKey("error")' }, + { label: 'Amount range:', code: 'o => o.Amount.Amount >= -100 && o.Amount.Amount <= -10' } + ]; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + } + + onCriteriaSubmitted(criteria: string): void { + this.currentCriteria = criteria; + this.deleteOperations(criteria); + } + + deleteOperations(criteria: string): void { + const confirmMessage = `Are you sure you want to delete all operations matching the criteria:\n\n${criteria}\n\nThis action cannot be undone.`; + + const confirmed = confirm(confirmMessage); + if (!confirmed) return; + + this.isLoading = true; + this.deleteResult = null; + + this.operationsApi.removeOperations(this.budgetId, { criteria }).subscribe({ + next: (result) => { + this.isLoading = false; + this.deleteResult = { + errors: result.errors, + successes: result.successes + }; + + if (result.errors.length === 0) { + this.notificationService.showSuccess('Operations deleted successfully').subscribe(); + this.operationsApi.triggerRefresh(this.budgetId); + } else { + const errorMessage = result.errors.length > 5 + ? `Deletion completed with ${result.errors.length} errors. Check the results below.` + : `Deletion completed with errors. See details below.`; + this.notificationService.showError(errorMessage).subscribe(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to delete operations'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + viewOperations(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + resetResult(): void { + this.deleteResult = null; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.html new file mode 100644 index 00000000..4224e97d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.html @@ -0,0 +1,70 @@ +
+ +
+
+

Duplicate Operations

+
+ + +
+
+ + + + + + +
+ @if (duplicateGroups.length > 0) { +
+

Found {{ duplicateGroups.length }} duplicate groups ({{ getTotalDuplicates() }} operations)

+
+ + @for (group of duplicateGroups; track $index; let groupIdx = $index) { +
+

Duplicate Group {{ groupIdx + 1 }} ({{ group.length }} operations)

+ + +
+ } + } @else { +
+

No duplicate operations found with the current criteria.

+ +
+ } +
+
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.less new file mode 100644 index 00000000..2e47d1e2 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.less @@ -0,0 +1,53 @@ +@import '../shared/styles/mixins.less'; + +.duplicates-container { + .page-container(); +} + +.header { + .header-with-actions(); + margin-bottom: 2rem; +} + +.duplicates-section { + margin-top: 2rem; +} + +.duplicates-summary { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--tui-warning-bg); + border-left: 3px solid var(--tui-warning-fill); + border-radius: 0.25rem; + + h3 { + margin: 0; + color: var(--tui-text-primary); + } +} + +.duplicate-group { + margin-bottom: 2rem; + + h4 { + margin: 0 0 0.75rem 0; + color: var(--tui-text-primary); + font-size: 1rem; + padding: 0.75rem 1rem; + background: var(--tui-warning-bg); + border-left: 3px solid var(--tui-warning-fill); + border-radius: 0.25rem 0.25rem 0 0; + } +} + +.empty-state { + padding: 3rem; + text-align: center; + color: var(--tui-text-secondary); + + p { + margin-bottom: 1rem; + font-size: 1.125rem; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.ts new file mode 100644 index 00000000..93bbc6ff --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/duplicates-list/duplicates-list.component.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { OperationResponse } from '../../budget/models'; +import { + TuiButton, + TuiLoader, + TuiTitle +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { OperationsTableComponent } from '../operations-table/operations-table.component'; +import { NotificationService } from '../shared/notification.service'; +import { OperationsHelperService } from '../shared/operations-helper.service'; +import { CriteriaFilterComponent } from '../shared/components/criteria-filter/criteria-filter.component'; +import { CriteriaExample } from '../shared/models/example.interface'; + +@Component({ + selector: 'app-duplicates-list', + standalone: true, + imports: [ + CommonModule, + TuiButton, + TuiLoader, + TuiCardLarge, + TuiTitle, + OperationsTableComponent, + CriteriaFilterComponent + ], + templateUrl: './duplicates-list.component.html', + styleUrls: ['./duplicates-list.component.less'] +}) +export class DuplicatesListComponent implements OnInit { + budgetId!: string; + duplicateGroups: OperationResponse[][] = []; + isLoading = false; + + criteriaExamples: CriteriaExample[] = [ + { label: 'All operations:', code: 'o => true' }, + { label: 'Specific year:', code: 'o => o.Timestamp.Year == 2024' }, + { label: 'Negative amounts only:', code: 'o => o.Amount.Amount < 0' }, + { label: 'By description contains:', code: 'o => o.Description.Contains("coffee")' }, + { label: 'Recent operations:', code: 'o => o.Timestamp > DateTime.Now.AddDays(-30)' } + ]; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService, + private operationsHelper: OperationsHelperService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + this.loadDuplicates('o => true'); + } + + loadDuplicates(criteria: string): void { + this.isLoading = true; + + this.operationsApi.getDuplicates(this.budgetId, criteria || undefined).subscribe({ + next: (groups) => { + this.duplicateGroups = groups; + this.isLoading = false; + }, + error: (error) => { + const errorMessage = this.notificationService.handleError(error, 'Failed to load duplicates'); + this.notificationService.showError(errorMessage).subscribe(); + this.isLoading = false; + } + }); + } + + onCriteriaSubmitted(criteria: string): void { + this.loadDuplicates(criteria); + } + + onCriteriaCleared(): void { + this.loadDuplicates('o => true'); + } + + clearFilters(): void { + this.onCriteriaCleared(); + } + + navigateToOperations(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + navigateToBudget(): void { + this.router.navigate(['/budget', this.budgetId, 'details']); + } + + getTotalDuplicates(): number { + return this.duplicateGroups.reduce((total, group) => total + group.length, 0); + } + + onDeleteOperation(operation: OperationResponse): void { + const confirmMessage = `Are you sure you want to delete this operation?\n\n${operation.description}\n${operation.amount.value} ${operation.amount.currencyCode}\n\nThis action cannot be undone.`; + + if (!confirm(confirmMessage)) { + return; + } + + this.isLoading = true; + + this.operationsHelper.deleteOperation(this.budgetId, operation.id).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map((e: any) => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to delete operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation deleted successfully').subscribe(); + this.loadDuplicates('o => true'); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to delete operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + onUpdateOperation(operation: OperationResponse): void { + this.isLoading = true; + + this.operationsHelper.updateOperation(this.budgetId, operation).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map(e => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to update operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation updated successfully').subscribe(); + this.loadDuplicates('o => true'); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to update operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.html new file mode 100644 index 00000000..ed137408 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.html @@ -0,0 +1,151 @@ +
+ +
+

Import Operations

+ + @if (budget) { +
+
+

Upload CSV File

+

+ The CSV file will be parsed using the reading settings configured for this budget. + Make sure you have configured the reading settings before importing. +

+ +
+ + + @if (selectedFile) { + ✓ {{ selectedFile.name }} + } +
+
+ +
+ + + + +

+ Regex pattern to match reading settings. Leave empty to auto-match by filename. +

+
+ +
+ + + + +

+ Specify the confidence level for transfer detection: Exact, Likely, or leave empty for default. +

+
+ +
+ + +
+
+ + @if (importResult) { +
+

Import Results

+
+
+ Registered: + {{ importResult.registered }} +
+
+ Duplicates: + {{ importResult.duplicates }} +
+ @if (importResult.errors.length > 0) { +
+ Errors: + {{ importResult.errors.length }} +
+ } + @if (importResult.successes.length > 0) { +
+ Successes: + {{ importResult.successes.length }} +
+ } +
+ + + @if (importResult.duplicates > 0) { +
+
+

Duplicates ({{ importResult.duplicates }} groups)

+ +
+ @if (showDuplicates) { + @for (group of getDuplicatesList(); track $index; let groupIdx = $index) { +
+
Duplicate Group {{ groupIdx + 1 }} ({{ group.length }} operations)
+ +
+ } + } +
+ } + + + +
+ } + } @else { +
Loading budget...
+ } +
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.less new file mode 100644 index 00000000..bcf48239 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.less @@ -0,0 +1,84 @@ +@import '../shared/styles/mixins.less'; + +.import-container { + .page-container(); +} + +.import-card { + .card-base(); +} + +.import-form { + margin-top: 2rem; +} + +.form-section { + margin-bottom: 2rem; + + h3 { + margin-bottom: 1rem; + } +} + +.help-text { + margin: 0.5rem 0; + color: var(--tui-text-secondary); + font-size: 0.875rem; +} + +.file-input-container { + display: flex; + align-items: center; + gap: 1rem; + + .file-input { + display: none; + } + + .file-selected { + color: var(--tui-success-fill); + font-weight: 600; + } +} + +.form-actions { + display: flex; + gap: 1rem; + margin-top: 2rem; +} + +.import-result { + .result-section-base(); +} + +.result-stats { + .result-stats-base(); +} + +.section-header-with-toggle { + .section-header-with-toggle(); +} + +.duplicates-section { + margin-top: 1.5rem; + + h4 { + color: var(--tui-warning-fill); + } +} + +.duplicate-group { + margin-bottom: 1.5rem; + + h5 { + margin: 0 0 0.75rem 0; + color: var(--tui-text-primary); + font-size: 1rem; + padding: 0.75rem 1rem; + background: var(--tui-warning-bg); + border-left: 3px solid var(--tui-warning-fill); + border-radius: 0.25rem 0.25rem 0 0; + } +} + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.ts new file mode 100644 index 00000000..09e34762 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/import-operations/import-operations.component.ts @@ -0,0 +1,155 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { BudgetApiService } from '../../budget/budget-api.service'; +import { BudgetResponse } from '../../budget/models'; +import { + TuiButton, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { OperationsTableComponent } from '../operations-table/operations-table.component'; +import { NotificationService } from '../shared/notification.service'; +import { OperationResultComponent } from '../shared/components/operation-result/operation-result.component'; +import { ImportResult } from '../shared/models/result.interface'; + +@Component({ + selector: 'app-import-operations', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TuiButton, + TuiLoader, + TuiTextfield, + TuiLabel, + TuiCardLarge, + TuiTitle, + OperationsTableComponent, + OperationResultComponent + ], + templateUrl: './import-operations.component.html', + styleUrls: ['./import-operations.component.less'] +}) +export class ImportOperationsComponent implements OnInit { + budgetId!: string; + budget: BudgetResponse | null = null; + isLoading = false; + + importForm!: FormGroup; + selectedFile: File | null = null; + importResult: ImportResult | null = null; + + // Section toggles + showDuplicates = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private budgetApi: BudgetApiService, + private fb: FormBuilder, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + + this.importForm = this.fb.group({ + transferConfidenceLevel: [''], + filePattern: [''] + }); + + this.loadBudget(); + } + + loadBudget(): void { + this.isLoading = true; + this.budgetApi.getBudgetById(this.budgetId).subscribe({ + next: (budget) => { + this.budget = budget || null; + this.isLoading = false; + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to load budget'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + if (!input.files || input.files.length === 0) { + this.selectedFile = null; + return; + } + + this.selectedFile = input.files[0]; + } + + importCsv(): void { + if (!this.selectedFile || !this.budget) { + this.notificationService.showError('Please select a CSV file first').subscribe(); + return; + } + + this.isLoading = true; + this.importResult = null; + + const transferConfidenceLevel = this.importForm.value.transferConfidenceLevel || undefined; + const filePattern = this.importForm.value.filePattern || undefined; + + this.operationsApi.importOperations( + this.budgetId, + this.selectedFile, + this.budget.version, + transferConfidenceLevel, + filePattern + ).subscribe({ + next: (result) => { + this.isLoading = false; + this.importResult = { + registered: result.registeredOperations.length, + duplicates: result.duplicates.length, + errors: result.errors, + successes: result.successes, + duplicatesList: result.duplicates + }; + + if (result.errors.length === 0) { + this.notificationService.showSuccess(`Successfully imported ${result.registeredOperations.length} operations`).subscribe(); + this.operationsApi.triggerRefresh(this.budgetId); + } else { + const errorMessage = result.errors.length > 5 + ? `Import completed with ${result.errors.length} errors. Check the results below.` + : `Import completed with errors. See details below.`; + this.notificationService.showError(errorMessage).subscribe(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to import operations'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + viewOperations(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + toggleDuplicates(): void { + this.showDuplicates = !this.showDuplicates; + } + + getDuplicatesList(): any[] { + return this.importResult?.duplicatesList || []; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.html new file mode 100644 index 00000000..643c8aed --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.html @@ -0,0 +1,45 @@ +
+ +
+
+

{{ groupTitle }}

+ +
+ + @if (operations.length > 0) { +
+
+ {{ operations.length }} operations +
+ + + +
+ } @else { +
+

No operations found for this group.

+ +
+ } +
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.less new file mode 100644 index 00000000..d0a3286a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.less @@ -0,0 +1,48 @@ +@import '../shared/styles/mixins.less'; + +.logbook-group-container { + .page-container(); +} + +.logbook-group-card { + .card-base(); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; + + h2 { + margin: 0; + flex: 1; + } +} + +.operations-section { + margin-top: 1rem; +} + +.operations-count { + margin-bottom: 1rem; + padding: 0.75rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; + text-align: center; + font-weight: 600; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + + p { + margin-bottom: 1.5rem; + color: var(--tui-text-secondary); + font-size: 1.125rem; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.ts new file mode 100644 index 00000000..407ff4d4 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-group/logbook-group.component.ts @@ -0,0 +1,186 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { + TuiButton, + TuiLoader, + TuiTitle +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { NotificationService } from '../shared/notification.service'; +import { OperationsHelperService } from '../shared/operations-helper.service'; +import { OperationsTableComponent } from '../operations-table/operations-table.component'; +import { LogbookResponse, OperationResponse } from '../../budget/models'; + +@Component({ + selector: 'app-logbook-group', + standalone: true, + imports: [ + CommonModule, + TuiButton, + TuiLoader, + TuiCardLarge, + TuiTitle, + OperationsTableComponent + ], + templateUrl: './logbook-group.component.html', + styleUrls: ['./logbook-group.component.less'] +}) +export class LogbookGroupComponent implements OnInit { + budgetId!: string; + rangeName!: string; + criteriaPath!: string; + fromDate!: string; + tillDate!: string; + criteria?: string; + cronExpression?: string; + + isLoading = false; + operations: OperationResponse[] = []; + groupTitle = ''; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService, + private operationsHelper: OperationsHelperService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + this.rangeName = this.route.snapshot.queryParams['rangeName'] || ''; + this.criteriaPath = this.route.snapshot.queryParams['criteriaPath'] || ''; + this.fromDate = this.route.snapshot.queryParams['from'] || ''; + this.tillDate = this.route.snapshot.queryParams['till'] || ''; + this.criteria = this.route.snapshot.queryParams['criteria']; + this.cronExpression = this.route.snapshot.queryParams['cronExpression']; + + const pathParts = this.criteriaPath.split('/'); + const criteriaName = pathParts[pathParts.length - 1] || 'Group'; + this.groupTitle = `${criteriaName} - ${this.rangeName}`; + + this.loadOperations(); + } + + loadOperations(): void { + this.isLoading = true; + + const from = this.fromDate ? new Date(this.fromDate) : undefined; + const till = this.tillDate ? new Date(this.tillDate) : undefined; + + this.operationsApi.getLogbook( + this.budgetId, + from, + till, + this.criteria, + this.cronExpression + ).subscribe({ + next: (result: LogbookResponse) => { + this.isLoading = false; + + // Find the specific range and criteria path + const rangedEntry = result.ranges.find(r => r.range.name === this.rangeName); + if (rangedEntry) { + const entry = this.findEntryByPath(rangedEntry.entry, this.criteriaPath); + if (entry) { + this.operations = entry.operations || []; + } + } + + if (this.operations.length === 0) { + this.notificationService.showWarning('No operations found for this group').subscribe(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to load operations'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + private findEntryByPath(entry: any, targetPath: string): any { + const currentPath = entry.description; + + if (currentPath === targetPath) { + return entry; + } + + if (targetPath.startsWith(currentPath + '/')) { + const remainingPath = targetPath.substring(currentPath.length + 1); + + for (const child of (entry.children || [])) { + const found = this.findEntryByPath(child, remainingPath); + if (found) return found; + } + } + + return null; + } + + backToLogbook(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'logbook'], { + queryParams: { + from: this.fromDate, + till: this.tillDate, + criteria: this.criteria, + cronExpression: this.cronExpression + } + }); + } + + onDeleteOperation(operation: OperationResponse): void { + const confirmMessage = `Are you sure you want to delete this operation?\n\n${operation.description}\n${operation.amount.value} ${operation.amount.currencyCode}\n\nThis action cannot be undone.`; + + if (!confirm(confirmMessage)) { + return; + } + + this.isLoading = true; + + this.operationsHelper.deleteOperation(this.budgetId, operation.id).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map((e: any) => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to delete operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation deleted successfully').subscribe(); + this.loadOperations(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to delete operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + onUpdateOperation(operation: OperationResponse): void { + this.isLoading = true; + + this.operationsHelper.updateOperation(this.budgetId, operation).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map(e => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to update operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation updated successfully').subscribe(); + this.loadOperations(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to update operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-state.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-state.service.ts new file mode 100644 index 00000000..ff49c154 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-state.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; + +interface LogbookState { + expandedRows: Set; + scrollPosition: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class LogbookStateService { + private stateMap = new Map(); + + saveState(budgetId: string, expandedRows: Set, scrollPosition: number): void { + this.stateMap.set(budgetId, { + expandedRows: new Set(expandedRows), + scrollPosition + }); + } + + getState(budgetId: string): LogbookState | undefined { + return this.stateMap.get(budgetId); + } + + clearState(budgetId: string): void { + this.stateMap.delete(budgetId); + } + + hasState(budgetId: string): boolean { + return this.stateMap.has(budgetId); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.html new file mode 100644 index 00000000..6e0c8765 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.html @@ -0,0 +1,215 @@ +
+ +
+

Operations Logbook

+ +
+

Filters

+ + + +
+ + + + +
+ +
+ + + + +
+ + + + + + +
+ + + + +
+ +
+
+ +
+ +
+ + @if (logbook) { +
+

Logbook Statistics

+ + @if (criteriaRows.length > 0 && ranges.length > 0) { +
+ + + + + @for (range of ranges; track range.name) { + + } + + + + @for (row of criteriaRows; track row.path) { + @if (isRowVisible(row)) { + + + @for (range of ranges; track range.name) { + + } + } + } + +
Category + {{ range.name }} +
+
+ @if (row.hasChildren) { + + {{ isRowExpanded(row.path) ? '▼' : '▶' }} + + } @else { + + } + {{ row.description }} +
+
+ @if (getEntryForRange(row, range.name); as entry) { + @if (entry.sum.value !== 0) { +
+ {{ entry.sum.value | currencyFormat }} +
+ } @else { +
-
+ } + } @else { +
-
+ } +
+
+ } @else { +
+

No logbook data available for the selected criteria and date range.

+
+ } + + @if (logbook.errors.length > 0) { +
+

Errors ({{ logbook.errors.length }})

+
+ @for (error of logbook.errors; track $index) { +
+
+ {{ error.message || 'Unknown error' }} +
+ + @if (hasMetadata(error)) { + + } + + @if (hasNestedReasons(error)) { +
+
Nested Reasons:
+
+ @for (reason of error.reasons; track $index) { +
+ {{ reason.message || 'No message' }} + @if (hasMetadata(reason)) { + + } +
+ } +
+
+ } +
+ } +
+
+ } +
+ } +
+
+
diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.less new file mode 100644 index 00000000..3e9d4665 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.less @@ -0,0 +1,329 @@ +@import '../shared/styles/mixins.less'; + +.logbook-container { + .page-container(); +} + +.logbook-card { + .card-base(); +} + +.filters-section { + .filters-section-base(); + + h3 { + margin-bottom: 1rem; + } +} + +.additional-parameters { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.date-range-section { + padding: 1rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; +} + +.date-inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 1rem; +} + +.date-field { + display: flex; + flex-direction: column; + min-width: 13rem; +} + +.cron-section { + padding: 1rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; +} + +.cron-field { + margin-bottom: 0.5rem; +} + +.action-buttons { + display: flex; + gap: 1rem; + margin: 2rem 0; + flex-wrap: wrap; +} + +.logbook-section { + margin-top: 2rem; + + h3 { + margin-bottom: 1.5rem; + } +} + +.logbook-table-container { + overflow-x: auto; + border: 1px solid var(--tui-border-normal); + border-radius: 0.5rem; + background: var(--tui-background-base); +} + +.logbook-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + + thead { + background: var(--tui-background-accent-1); + position: sticky; + top: 0; + z-index: 10; + + th { + padding: 1rem; + text-align: left; + font-weight: 700; + color: var(--tui-text-primary); + border-bottom: 2px solid var(--tui-primary); + border-right: 1px solid var(--tui-border-normal); + white-space: nowrap; + + &:last-child { + border-right: none; + } + + &.criteria-column { + min-width: 250px; + position: sticky; + left: 0; + background: var(--tui-background-accent-1); + z-index: 11; + } + + &.range-column { + min-width: 70px; + } + } + } + + tbody { + tr.criteria-row { + border-bottom: 1px solid var(--tui-border-normal); + transition: background-color 0.15s; + + &:hover { + background: var(--tui-background-neutral-1); + } + + &[data-level="0"] { + background: var(--tui-background-neutral-1); + font-weight: 600; + } + + &[data-level="1"] { + background: var(--tui-background-base); + } + + td { + padding: 0.75rem 1rem; + vertical-align: middle; + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + } + } + } +} + +.criteria-cell { + position: sticky; + left: 0; + background: inherit; + z-index: 5; + min-width: 250px; +} + +.criteria-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.expand-icon { + width: 1rem; + text-align: center; + color: var(--tui-text-secondary); + font-size: 0.75rem; + cursor: pointer; + + &.placeholder { + visibility: hidden; + } + + &:hover { + color: var(--tui-text-primary); + } +} + +.criteria-name { + font-size: 1rem; +} + +.data-cell { + min-width: 120px; + text-align: right; + + &.clickable { + cursor: pointer; + transition: all 0.15s; + + &:hover { + background: var(--tui-background-neutral-2); + transform: scale(1.02); + } + } +} + +.sum-value { + font-family: monospace; + font-weight: 700; + font-size: 1rem; + + &.positive { + color: var(--tui-success-fill); + } + + &.negative { + color: var(--tui-error-fill); + } +} + +.empty-cell { + text-align: center; + color: var(--tui-text-tertiary); + font-style: italic; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--tui-text-secondary); + + p { + margin: 0; + font-size: 1.125rem; + } +} + +.logbook-errors { + margin-top: 2rem; + padding: 1.5rem; + background: var(--tui-error-bg); + border: 2px solid var(--tui-error-fill); + border-radius: 0.5rem; + + h4 { + margin: 0 0 1rem 0; + color: var(--tui-error-fill); + } +} + +.error-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.error-item { + padding: 1rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + color: var(--tui-text-primary); + font-size: 0.875rem; + border-left: 3px solid var(--tui-error-fill); +} + +.error-message { + margin-bottom: 0.75rem; + font-size: 1rem; + + strong { + color: var(--tui-text-primary); + } +} + +.error-metadata, +.nested-reasons { + margin-top: 0.75rem; + padding: 0.75rem; + background: var(--tui-background-neutral-1); + border-radius: 0.25rem; +} + +.metadata-label, +.nested-label { + font-weight: 600; + color: var(--tui-text-secondary); + margin-bottom: 0.5rem; + font-size: 0.8125rem; + text-transform: uppercase; +} + +.metadata-content { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.metadata-item { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.75rem; + padding: 0.5rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + + &.small { + padding: 0.25rem 0.5rem; + font-size: 0.8125rem; + } +} + +.metadata-key { + font-weight: 600; + color: var(--tui-text-secondary); + white-space: nowrap; +} + +.metadata-value { + font-family: monospace; + color: var(--tui-text-primary); + word-break: break-word; + white-space: pre-wrap; +} + +.nested-content { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.nested-reason { + padding: 0.75rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + border-left: 2px solid var(--tui-border-normal); +} + +.reason-metadata { + margin-top: 0.5rem; + padding-left: 0.5rem; +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.ts new file mode 100644 index 00000000..56692dd0 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/logbook-view/logbook-view.component.ts @@ -0,0 +1,398 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { + TuiButton, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { TuiAccordion } from '@taiga-ui/kit'; +import { NotificationService } from '../shared/notification.service'; +import { CriteriaFilterComponent } from '../shared/components/criteria-filter/criteria-filter.component'; +import { ExamplesSectionComponent } from '../shared/components/examples-section/examples-section.component'; +import { CriteriaExample } from '../shared/models/example.interface'; +import { LogbookEntryResponse, LogbookResponse, RangedLogbookEntryResponse, NamedRangeResponse } from '../../budget/models'; +import { LogbookStateService } from './logbook-state.service'; + +interface CriteriaRow { + description: string; + path: string; + level: number; + rangeData: Map; + hasChildren: boolean; +} +import { DateFormatPipe } from '../shared/pipes/date-format.pipe'; +import { CurrencyFormatPipe } from '../shared/pipes/currency-format.pipe'; + +@Component({ + selector: 'app-logbook-view', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TuiButton, + TuiLoader, + TuiCardLarge, + TuiTitle, + TuiTextfield, + TuiLabel, + TuiAccordion, + CriteriaFilterComponent, + DateFormatPipe, + CurrencyFormatPipe + ], + templateUrl: './logbook-view.component.html', + styleUrls: ['./logbook-view.component.less'] +}) +export class LogbookViewComponent implements OnInit { + budgetId!: string; + isLoading = false; + logbook: LogbookResponse | null = null; + + currentCriteria = ''; + fromDate: string = ''; + tillDate: string = ''; + cronExpression = ''; + + ranges: NamedRangeResponse[] = []; + criteriaRows: CriteriaRow[] = []; + expandedRows = new Set(); + + criteriaExamples: CriteriaExample[] = [ + { label: 'All operations:', code: 'o => true' }, + { label: 'Positive amounts:', code: 'o => o.Amount.Amount > 0' }, + { label: 'Negative amounts:', code: 'o => o.Amount.Amount < 0' }, + { label: 'Contains text:', code: 'o => o.Description.Contains("groceries")' }, + { label: 'By tag:', code: 'o => o.Tags.Any(t => t.Value == "food")' }, + { label: 'Amount range:', code: 'o => o.Amount.Amount >= -1000 && o.Amount.Amount <= -100' } + ]; + + cronExamples: CriteriaExample[] = [ + { label: 'Daily:', code: '0 0 * * *' }, + { label: 'Weekly (Mondays):', code: '0 0 * * 1' }, + { label: 'Monthly (1st day):', code: '0 0 1 * *' }, + { label: 'Bi-weekly:', code: '0 0 1,15 * *' }, + { label: 'Quarterly:', code: '0 0 1 1,4,7,10 *' } + ]; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService, + private logbookStateService: LogbookStateService + ) { + this.resetToDefaultDateRange(); + } + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + + // Restore filter parameters from query params if present + const queryParams = this.route.snapshot.queryParams; + + if (queryParams['from']) { + this.fromDate = queryParams['from']; + } + + if (queryParams['till']) { + this.tillDate = queryParams['till']; + } + + if (queryParams['criteria']) { + this.currentCriteria = queryParams['criteria']; + } + + if (queryParams['cronExpression']) { + this.cronExpression = queryParams['cronExpression']; + } + + this.loadLogbook(); + } + + onCriteriaSubmitted(criteria: string): void { + this.currentCriteria = criteria; + this.clearStateAndReload(); + } + + onCriteriaCleared(): void { + this.currentCriteria = ''; + this.cronExpression = ''; + this.resetToDefaultDateRange(); + this.clearStateAndReload(); + } + + private clearStateAndReload(): void { + // Clear saved state when filters change + this.logbookStateService.clearState(this.budgetId); + this.updateUrlAndLoadLogbook(); + } + + private updateUrlAndLoadLogbook(): void { + // Update URL with current filter state + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + from: this.fromDate || undefined, + till: this.tillDate || undefined, + criteria: this.currentCriteria || undefined, + cronExpression: this.cronExpression || undefined + }, + queryParamsHandling: 'merge' + }); + + this.loadLogbook(); + } + + private resetToDefaultDateRange(): void { + const now = new Date(); + const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); + const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); + + this.fromDate = firstDay.toISOString().slice(0, 16); + this.tillDate = lastDay.toISOString().slice(0, 16); + } + + loadLogbook(): void { + this.isLoading = true; + this.logbook = null; + this.ranges = []; + this.criteriaRows = []; + + const from = this.fromDate ? new Date(this.fromDate) : undefined; + const till = this.tillDate ? new Date(this.tillDate) : undefined; + + this.operationsApi.getLogbook( + this.budgetId, + from, + till, + this.currentCriteria || undefined, + this.cronExpression || undefined + ).subscribe({ + next: (result) => { + this.isLoading = false; + this.logbook = result; + + if (result.ranges && result.ranges.length > 0) { + this.ranges = result.ranges.map(r => r.range); + this.criteriaRows = this.buildCriteriaRows(result.ranges); + + // Restore expansion state if it exists, otherwise start fresh + const savedState = this.logbookStateService.getState(this.budgetId); + if (savedState) { + this.expandedRows = savedState.expandedRows; + + // Restore scroll position after view is rendered + setTimeout(() => { + window.scrollTo(0, savedState.scrollPosition); + }, 100); + } else { + this.expandedRows.clear(); + } + } + + if (result.errors.length > 0) { + const errorMessage = `Logbook loaded with ${result.errors.length} errors`; + this.notificationService.showWarning(errorMessage).subscribe(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to load logbook'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + private buildCriteriaRows(rangedEntries: RangedLogbookEntryResponse[]): CriteriaRow[] { + const rows: CriteriaRow[] = []; + + // Get all unique criteria paths from the first range to establish row structure + if (rangedEntries.length === 0) return rows; + + const firstEntry = rangedEntries[0].entry; + this.collectCriteriaPaths(firstEntry, '', 0, rows, rangedEntries); + + return rows; + } + + private collectCriteriaPaths( + entry: LogbookEntryResponse, + parentPath: string, + level: number, + rows: CriteriaRow[], + rangedEntries: RangedLogbookEntryResponse[] + ): void { + const currentPath = parentPath ? `${parentPath}/${entry.description}` : entry.description; + + // Create range data map for this criteria + const rangeData = new Map(); + + for (const rangedEntry of rangedEntries) { + const entryData = this.findEntryByPath(rangedEntry.entry, currentPath); + if (entryData) { + rangeData.set(rangedEntry.range.name, entryData); + } + } + + const hasChildren = entry.children && entry.children.length > 0; + + rows.push({ + description: entry.description, + path: currentPath, + level, + rangeData, + hasChildren + }); + + // Recursively add children + if (hasChildren) { + for (const child of entry.children) { + this.collectCriteriaPaths(child, currentPath, level + 1, rows, rangedEntries); + } + } + } + + private findEntryByPath(entry: LogbookEntryResponse, targetPath: string): LogbookEntryResponse | null { + const currentPath = entry.description; + + if (currentPath === targetPath) { + return entry; + } + + if (targetPath.startsWith(currentPath + '/')) { + const remainingPath = targetPath.substring(currentPath.length + 1); + + for (const child of entry.children) { + const found = this.findEntryByPath(child, remainingPath); + if (found) return found; + } + } + + return null; + } + + toggleRow(path: string): void { + if (this.expandedRows.has(path)) { + this.expandedRows.delete(path); + } else { + this.expandedRows.add(path); + } + } + + isRowExpanded(path: string): boolean { + return this.expandedRows.has(path); + } + + isRowVisible(row: CriteriaRow): boolean { + if (row.level === 0) return true; + + // Check if all parent rows are expanded + const pathParts = row.path.split('/'); + for (let i = 1; i < pathParts.length; i++) { + const parentPath = pathParts.slice(0, i).join('/'); + if (!this.expandedRows.has(parentPath)) { + return false; + } + } + return true; + } + + viewGroupOperations(row: CriteriaRow, rangeName: string, event: Event): void { + event.stopPropagation(); + + // Save current state before navigating + this.logbookStateService.saveState( + this.budgetId, + this.expandedRows, + window.scrollY + ); + + this.router.navigate(['/budget', this.budgetId, 'operations', 'logbook', 'group'], { + queryParams: { + rangeName: rangeName, + criteriaPath: row.path, + from: this.fromDate, + till: this.tillDate, + criteria: this.currentCriteria || undefined, + cronExpression: this.cronExpression || undefined + } + }); + } + + getEntryForRange(row: CriteriaRow, rangeName: string): LogbookEntryResponse | undefined { + return row.rangeData.get(rangeName); + } + + hasOperationsInRange(row: CriteriaRow, rangeName: string): boolean { + const entry = this.getEntryForRange(row, rangeName); + return entry ? (entry.operations && entry.operations.length > 0) : false; + } + + viewOperations(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + setDateRange(type: 'currentMonth' | 'lastMonth' | 'currentYear' | 'lastYear'): void { + const now = new Date(); + let from: Date; + let till: Date; + + switch (type) { + case 'currentMonth': + from = new Date(now.getFullYear(), now.getMonth(), 1); + till = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); + break; + case 'lastMonth': + from = new Date(now.getFullYear(), now.getMonth() - 1, 1); + till = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59); + break; + case 'currentYear': + from = new Date(now.getFullYear(), 0, 1); + till = new Date(now.getFullYear(), 11, 31, 23, 59, 59); + break; + case 'lastYear': + from = new Date(now.getFullYear() - 1, 0, 1); + till = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59); + break; + } + + this.fromDate = from.toISOString().slice(0, 16); + this.tillDate = till.toISOString().slice(0, 16); + this.clearStateAndReload(); + } + + hasMetadata(error: any): boolean { + return error.metadata && Object.keys(error.metadata).length > 0; + } + + getMetadataKeys(metadata: Record): string[] { + return Object.keys(metadata); + } + + formatMetadataValue(value: any): string { + if (value === null || value === undefined) { + return 'null'; + } + if (typeof value === 'object') { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } + } + return String(value); + } + + hasNestedReasons(error: any): boolean { + return error.reasons && error.reasons.length > 0; + } +} + + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-api.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-api.service.ts new file mode 100644 index 00000000..b86b1653 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-api.service.ts @@ -0,0 +1,242 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { BehaviorSubject, Observable, startWith, switchMap } from 'rxjs'; +import { + OperationResponse, + UpdateOperationsRequest, + RemoveOperationsRequest, + RetagOperationsRequest, + ImportResultResponse, + UpdateResultResponse, + DeleteResultResponse, + RetagResultResponse, + LogbookResponse, + TransfersListResponse, + RegisterTransfersRequest, + RemoveTransfersRequest +} from '../budget/models'; +import { AppConfigService } from '../config/app-config.service'; + +@Injectable({ + providedIn: 'root' +}) +export class OperationsApiService { + public readonly baseUrl: string; + private refresh$ = new BehaviorSubject(null); + + private static readonly jsonHeaders = new HttpHeaders().set('Content-Type', 'application/json'); + + constructor( + private http: HttpClient, + private configService: AppConfigService + ) { + this.baseUrl = this.configService.apiUrl + '/api/v0.1'; + } + + /** + * Get all operations for a specific budget + */ + getOperations( + budgetId: string, + criteria?: string, + outputCurrency?: string, + excludeTransfers: boolean = false + ): Observable { + return this.refresh$.pipe( + startWith(undefined), + switchMap(() => { + let url = `${this.baseUrl}/budget/${budgetId}/operations`; + const params = new URLSearchParams(); + + if (criteria) { + params.append('criteria', criteria); + } + if (outputCurrency) { + params.append('outputCurrency', outputCurrency); + } + if (excludeTransfers) { + params.append('excludeTransfers', 'true'); + } + + const queryString = params.toString(); + if (queryString) { + url += `?${queryString}`; + } + + return this.http.get(url, { withCredentials: true }); + }) + ); + } + + /** + * Import operations into a budget from CSV file + */ + importOperations( + budgetId: string, + file: File, + budgetVersion: string, + transferConfidenceLevel?: string, + filePattern?: string + ): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('budgetVersion', budgetVersion); + if (transferConfidenceLevel) { + formData.append('transferConfidenceLevel', transferConfidenceLevel); + } + if (filePattern) { + formData.append('filePattern', filePattern); + } + + return this.http.post( + `${this.baseUrl}/budget/${budgetId}/operations/import`, + formData, + { withCredentials: true } + ); + } + + /** + * Update existing operations + */ + updateOperations(budgetId: string, request: UpdateOperationsRequest): Observable { + return this.http.put( + `${this.baseUrl}/budget/${budgetId}/operations`, + request, + { headers: OperationsApiService.jsonHeaders, withCredentials: true } + ); + } + + /** + * Remove operations matching criteria + */ + removeOperations(budgetId: string, request: RemoveOperationsRequest): Observable { + return this.http.request( + 'DELETE', + `${this.baseUrl}/budget/${budgetId}/operations`, + { + headers: OperationsApiService.jsonHeaders, + body: request, + withCredentials: true + } + ); + } + + /** + * Retag operations matching criteria + */ + retagOperations(budgetId: string, request: RetagOperationsRequest): Observable { + return this.http.post( + `${this.baseUrl}/budget/${budgetId}/operations/retag`, + request, + { headers: OperationsApiService.jsonHeaders, withCredentials: true } + ); + } + + /** + * Trigger refresh for operations list + */ + triggerRefresh(budgetId: string): void { + this.refresh$.next(budgetId); + } + + /** + * Get duplicate operations + */ + getDuplicates(budgetId: string, criteria?: string): Observable { + const params: any = {}; + if (criteria) { + params.criteria = criteria; + } + return this.http.get( + `${this.baseUrl}/budget/${budgetId}/operations/duplicates`, + { params, withCredentials: true } + ); + } + + /** + * Get logbook (aggregated operations statistics) + */ + getLogbook( + budgetId: string, + from?: Date, + till?: Date, + criteria?: string, + cronExpression?: string + ): Observable { + const params: any = {}; + if (from) { + params.from = from.toISOString(); + } + if (till) { + params.till = till.toISOString(); + } + if (criteria) { + params.criteria = criteria; + } + if (cronExpression) { + params.cronExpression = cronExpression; + } + return this.http.get( + `${this.baseUrl}/budget/${budgetId}/operations/logbook`, + { params, withCredentials: true } + ); + } + + /** + * Search transfers in a budget + */ + searchTransfers( + budgetId: string, + from?: Date, + till?: Date, + accuracy?: string + ): Observable { + const params: any = {}; + if (from) { + params.from = from.toISOString(); + } + if (till) { + params.till = till.toISOString(); + } + if (accuracy) { + params.accuracy = accuracy; + } + return this.http.get( + `${this.baseUrl}/budget/${budgetId}/transfers`, + { params, withCredentials: true } + ); + } + + /** + * Register new transfers + */ + registerTransfers( + budgetId: string, + request: RegisterTransfersRequest + ): Observable { + return this.http.post( + `${this.baseUrl}/budget/${budgetId}/transfers`, + request, + { headers: OperationsApiService.jsonHeaders, withCredentials: true } + ); + } + + /** + * Remove transfers + */ + removeTransfers( + budgetId: string, + request: RemoveTransfersRequest + ): Observable { + return this.http.request( + 'DELETE', + `${this.baseUrl}/budget/${budgetId}/transfers`, + { + headers: OperationsApiService.jsonHeaders, + body: request, + withCredentials: true + } + ); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.html new file mode 100644 index 00000000..3a63ba14 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.html @@ -0,0 +1,134 @@ +
+
+
+

Operations

+
+ + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+ + +
+ @if (operations$ | async; as ops) { + @if (ops.length > 0) { + + + +
+ Total: {{ operations.length }} operations +
+ } @else { +
+

No operations found.

+ +
+ } + } @else { + +
Loading operations...
+
+ } +
+
+
diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.less new file mode 100644 index 00000000..8b639dce --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.less @@ -0,0 +1,244 @@ +@import '../shared/styles/mixins.less'; + +.operations-container { + .page-container(); +} + +.operations-card { + .card-base(); +} + +.header { + .header-with-actions(); +} + +.currency-selector { + min-width: 12rem; +} + +.operations-section { + margin-top: 2rem; +} + +.operations-table-container { + overflow-x: auto; + border: 1px solid var(--tui-border-normal); + border-radius: 0.5rem; +} + +.operations-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + + thead { + background: var(--tui-background-neutral-1); + + th { + padding: 0.75rem 1rem; + text-align: left; + font-weight: 600; + color: var(--tui-text-primary); + border-bottom: 1px solid var(--tui-border-normal); + border-right: 1px solid var(--tui-border-normal); + white-space: nowrap; + + &:last-child { + border-right: none; + } + + &.col-timestamp { + width: 180px; + } + + &.col-description { + width: auto; + min-width: 300px; + } + + &.col-amount { + width: 150px; + text-align: right; + white-space: nowrap; + } + + &.col-tags { + width: 150px; + } + + &.col-attributes { + width: 250px; + } + + &.col-actions { + width: 80px; + text-align: center; + } + } + } + + tbody { + tr.operation-row { + border-bottom: 1px solid var(--tui-border-normal); + transition: background-color 0.15s; + + &:hover { + background: var(--tui-background-neutral-1); + } + + td { + padding: 0.75rem 1rem; + vertical-align: middle; + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + + &.col-timestamp { + font-family: monospace; + font-size: 0.875rem; + color: var(--tui-text-secondary); + } + + &.col-description { + color: var(--tui-text-primary); + } + + &.col-amount { + text-align: right; + font-weight: 700; + font-family: monospace; + white-space: nowrap; + + &.positive { + color: var(--tui-success-fill); + } + + &.negative { + color: var(--tui-error-fill); + } + } + + &.col-tags { + overflow: hidden; + + .tags-inline { + display: flex; + flex-wrap: nowrap; + gap: 0.25rem; + overflow-x: auto; + scrollbar-width: thin; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-thumb { + background: var(--tui-border-normal); + border-radius: 2px; + } + } + } + + &.col-attributes { + overflow: hidden; + + .attributes-inline { + display: flex; + flex-wrap: nowrap; + gap: 0.25rem; + overflow-x: auto; + scrollbar-width: thin; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-thumb { + background: var(--tui-border-normal); + border-radius: 2px; + } + } + + .no-attributes { + color: var(--tui-text-tertiary); + font-style: italic; + } + } + + &.col-actions { + text-align: center; + } + } + } + + tr.operation-details-row { + background: var(--tui-background-base); + border-bottom: 1px solid var(--tui-border-normal); + + td { + padding: 0; + } + } + } +} + +.operation-details { + padding: 1.5rem; +} + +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.detail-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.detail-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--tui-text-secondary); + text-transform: uppercase; +} + +.detail-value { + background: var(--tui-background-neutral-1); + padding: 0.5rem; + border-radius: 0.25rem; + font-family: monospace; + font-size: 0.875rem; + word-break: break-all; +} + +.operations-count { + margin-top: 1rem; + padding: 0.75rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; + text-align: center; + font-weight: 600; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; + + p { + margin-bottom: 1.5rem; + color: var(--tui-text-secondary); + font-size: 1.125rem; + } +} + +.loading-placeholder { + text-align: center; + padding: 3rem 1rem; + color: var(--tui-text-secondary); +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.ts new file mode 100644 index 00000000..a7300dc1 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-list/operations-list.component.ts @@ -0,0 +1,190 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, catchError, of } from 'rxjs'; +import { OperationsApiService } from '../operations-api.service'; +import { OperationResponse } from '../../budget/models'; +import { + TuiButton, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import {TuiCheckbox, TuiChevron, TuiDataListWrapper, TuiSelect} from '@taiga-ui/kit'; +import { OperationsTableComponent } from '../operations-table/operations-table.component'; +import { NotificationService } from '../shared/notification.service'; +import { OperationsHelperService } from '../shared/operations-helper.service'; +import { CriteriaFilterComponent } from '../shared/components/criteria-filter/criteria-filter.component'; +import { CriteriaExample } from '../shared/models/example.interface'; + +@Component({ + selector: 'app-operations-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TuiButton, + TuiLoader, + TuiTextfield, + TuiChevron, + TuiDataListWrapper, + TuiLabel, + TuiCardLarge, + TuiTitle, + TuiCheckbox, + OperationsTableComponent, + CriteriaFilterComponent, + TuiSelect + ], + templateUrl: './operations-list.component.html', + styleUrls: ['./operations-list.component.less'] +}) +export class OperationsListComponent implements OnInit { + budgetId!: string; + operations$!: Observable; + operations: OperationResponse[] = []; + isLoading = false; + + currentCriteria = `o => o.Timestamp.Year == ${new Date().getFullYear()} && o.Timestamp.Month >= ${new Date().getMonth()}`; + outputCurrency = ''; + excludeTransfers = true; + + criteriaExamples: CriteriaExample[] = [ + { label: 'All operations:', code: 'o => true' }, + { label: 'Positive amounts:', code: 'o => o.Amount.Amount > 0' }, + { label: 'Negative amounts:', code: 'o => o.Amount.Amount < 0' }, + { label: 'Specific year:', code: 'o => o.Timestamp.Year == 2023' }, + { label: 'Contains text:', code: 'o => o.Description.Contains("groceries")' }, + { label: 'By tag:', code: 'o => o.Tags.Any(t => t.Value == "food")' }, + { label: 'Without tags:', code: 'o => o.Tags.Count == 0' }, + { label: 'Amount range:', code: 'o => o.Amount.Amount >= -1000 && o.Amount.Amount <= -100' } + ]; + + readonly items: string[] = ["RUB", "USD", "EUR"]; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService, + private operationsHelper: OperationsHelperService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + this.loadOperations(); + } + + loadOperations(): void { + this.operations$ = this.operationsApi.getOperations( + this.budgetId, + this.currentCriteria || undefined, + this.outputCurrency || undefined, + this.excludeTransfers + ).pipe( + catchError(error => { + const errorMessage = this.notificationService.handleError(error, 'Failed to load operations'); + this.notificationService.showError(errorMessage).subscribe(); + return of([]); + }) + ); + + // Subscribe to update the array for the table component + this.operations$.subscribe(ops => this.operations = ops); + } + + onCriteriaSubmitted(criteria: string): void { + this.currentCriteria = criteria; + this.loadOperations(); + } + + onCriteriaCleared(): void { + this.currentCriteria = ''; + this.outputCurrency = ''; + this.excludeTransfers = false; + this.loadOperations(); + } + + navigateToImport(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'import']); + } + + navigateToBudget(): void { + this.router.navigate(['/budget', this.budgetId, 'details']); + } + + navigateToDelete(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'delete']); + } + + navigateToDuplicates(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'duplicates']); + } + + navigateToRetag(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'retag']); + } + + navigateToLogbook(): void { + this.router.navigate(['/budget', this.budgetId, 'operations', 'logbook']); + } + + navigateToTransfers(): void { + this.router.navigate(['/budget', this.budgetId, 'transfers']); + } + + onDeleteOperation(operation: OperationResponse): void { + const confirmMessage = `Are you sure you want to delete this operation?\n\n${operation.description}\n${operation.amount.value} ${operation.amount.currencyCode}\n\nThis action cannot be undone.`; + + if (!confirm(confirmMessage)) { + return; + } + + this.isLoading = true; + + this.operationsHelper.deleteOperation(this.budgetId, operation.id).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map((e: any) => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to delete operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation deleted successfully').subscribe(); + this.loadOperations(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to delete operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + onUpdateOperation(operation: OperationResponse): void { + this.isLoading = true; + + this.operationsHelper.updateOperation(this.budgetId, operation).subscribe({ + next: (result) => { + this.isLoading = false; + + if (result.errors && result.errors.length > 0) { + const errorMessage = result.errors.map(e => e.message || 'Unknown error').join('; '); + this.notificationService.showError(`Failed to update operation: ${errorMessage}`).subscribe(); + } else { + this.notificationService.showSuccess('Operation updated successfully').subscribe(); + this.loadOperations(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to update operation'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.html new file mode 100644 index 00000000..0d5254e3 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.html @@ -0,0 +1,217 @@ +
+ + + + + + + + + @if (showActions) { + + } + + + + @for (operation of operations; track operation.id) { + + + + + + + @if (showActions) { + + } + + + @if (expandedOperationId === operation.id) { + + + + } + } + +
Date & TimeDescriptionAmountTagsAttributesActions
{{ operation.timestamp | dateFormat }} + @if (isEditing(operation.id) && editingOperation) { + + } @else { + {{ operation.description }} + } + + @if (isEditing(operation.id) && editingOperation) { +
+ + +
+ } @else { + {{ operation.amount.value | currencyFormat: operation.amount.currencyCode }} + } +
+ @if (isEditing(operation.id) && editingOperation) { +
+ @for (tag of editingOperation.tags; track trackByIndex($index); let i = $index) { +
+ + +
+ } + +
+ } @else { +
+ @for (tag of operation.tags; track tag) { + {{ tag }} + } +
+ } +
+ @if (isEditing(operation.id) && editingOperation) { +
+ @for (key of editingOperation.attributes | objectKeys; track key) { +
+ + + +
+ } + +
+ } @else { + @if (operation.attributes && (operation.attributes | objectKeys).length > 0) { +
+ @for (key of operation.attributes | objectKeys; track key) { + + {{ key }}: {{ operation.attributes![key] }} + + } +
+ } @else { + + } + } +
+ @if (isEditing(operation.id)) { +
+ + +
+ } @else { +
+ + + +
+ } +
+ +
+
+ Operation ID: + {{ operation.id }} +
+
+ Budget ID: + {{ operation.budgetId }} +
+
+ Version: + {{ operation.version }} +
+
+
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.less new file mode 100644 index 00000000..1cdd8618 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.less @@ -0,0 +1,270 @@ +@import '../shared/styles/mixins.less'; + +.operations-table-container { + overflow-x: auto; + border-radius: 0.5rem; + border: 1px solid var(--tui-border-normal); +} + +.operations-table { + width: 100%; + border-collapse: collapse; + background: var(--tui-background-base); + + thead { + background: var(--tui-background-neutral-1); + position: sticky; + top: 0; + z-index: 1; + + tr { + border-bottom: 2px solid var(--tui-border-normal); + } + + th { + padding: 0.75rem 1rem; + text-align: left; + font-weight: 600; + font-size: 0.875rem; + color: var(--tui-text-secondary); + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + + &.col-timestamp { + width: 180px; + } + + &.col-description { + width: auto; + min-width: 300px; + } + + &.col-amount { + width: 150px; + text-align: right; + white-space: nowrap; + } + + &.col-tags { + width: 150px; + } + + &.col-attributes { + width: 250px; + } + + &.col-actions { + width: 130px; + text-align: center; + } + } + } + + tbody { + tr.operation-row { + border-bottom: 1px solid var(--tui-border-normal); + transition: background-color 0.15s; + + &:hover { + background: var(--tui-background-neutral-1); + } + + &.editing { + background: var(--tui-primary-bg); + + &:hover { + background: var(--tui-primary-bg); + } + } + + td { + padding: 0.75rem 1rem; + vertical-align: top; + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + + &.col-timestamp { + font-family: monospace; + font-size: 0.875rem; + color: var(--tui-text-secondary); + } + + &.col-description { + color: var(--tui-text-primary); + } + + &.col-amount { + text-align: right; + font-weight: 700; + font-family: monospace; + white-space: nowrap; + + &.positive { + color: var(--tui-success-fill); + } + + &.negative { + color: var(--tui-error-fill); + } + } + + &.col-tags { + overflow: hidden; + + .tags-inline { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + max-height: 60px; + overflow-y: auto; + .custom-scrollbar(); + } + } + + &.col-attributes { + overflow: hidden; + + .attributes-inline { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + max-height: 60px; + overflow-y: auto; + .custom-scrollbar(); + } + + .attr-chip { + font-size: 0.75rem; + } + + .no-attributes { + color: var(--tui-text-tertiary); + font-style: italic; + } + } + + &.col-actions { + text-align: center; + + .actions-buttons { + display: flex; + gap: 0.25rem; + justify-content: center; + align-items: center; + + &.edit-mode { + gap: 0.5rem; + } + } + } + } + } + + tr.operation-details-row { + background: var(--tui-background-neutral-1); + + td { + padding: 0; + border-bottom: 1px solid var(--tui-border-normal); + } + } + } +} + +.operation-details { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.detail-row { + display: flex; + gap: 1rem; + + .detail-label { + font-weight: 600; + min-width: 120px; + color: var(--tui-text-secondary); + } + + .detail-value { + color: var(--tui-text-primary); + + code { + padding: 0.125rem 0.375rem; + background: var(--tui-background-neutral-2); + border-radius: 0.25rem; + font-family: monospace; + font-size: 0.875rem; + } + } +} + +// Edit mode styles +.edit-input { + width: 100%; + padding: 0.375rem 0.5rem; + border: 1px solid var(--tui-border-normal); + border-radius: 0.25rem; + background: var(--tui-background-base); + color: var(--tui-text-primary); + font-size: 0.875rem; + font-family: inherit; + + &:focus { + outline: none; + border-color: var(--tui-primary); + box-shadow: 0 0 0 2px var(--tui-background-accent-1); + } +} + +.amount-edit { + display: flex; + gap: 0.5rem; + align-items: center; + + .amount-input { + flex: 1; + min-width: 80px; + } + + .currency-input { + width: 50px; + text-align: center; + text-transform: uppercase; + } +} + +.tags-edit, +.attributes-edit { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.tag-edit-item, +.attribute-edit-item { + display: flex; + gap: 0.25rem; + align-items: center; + + .tag-input { + flex: 1; + } + + .attr-key-input { + flex: 0 0 80px; + } + + .attr-value-input { + flex: 1; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.ts new file mode 100644 index 00000000..046e7ffe --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/operations-table/operations-table.component.ts @@ -0,0 +1,132 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { OperationResponse } from '../../budget/models'; +import { TuiButton, TuiExpand, TuiTextfield } from '@taiga-ui/core'; +import { TuiChip } from '@taiga-ui/kit'; +import { CurrencyFormatPipe } from '../shared/pipes/currency-format.pipe'; +import { DateFormatPipe } from '../shared/pipes/date-format.pipe'; +import { ObjectKeysPipe } from '../shared/pipes/object-keys.pipe'; + +interface EditableOperation { + id: string; + description: string; + amount: number; + currencyCode: string; + tags: string[]; + attributes: Record; +} + +@Component({ + selector: 'app-operations-table', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TuiButton, + TuiExpand, + TuiChip, + TuiTextfield, + CurrencyFormatPipe, + DateFormatPipe, + ObjectKeysPipe + ], + templateUrl: './operations-table.component.html', + styleUrls: ['./operations-table.component.less'] +}) +export class OperationsTableComponent { + @Input() operations: OperationResponse[] = []; + @Input() showActions = true; + @Output() operationDeleted = new EventEmitter(); + @Output() operationUpdated = new EventEmitter(); + + expandedOperationId: string | null = null; + editingOperationId: string | null = null; + editingOperation: EditableOperation | null = null; + + toggleOperationDetails(operationId: string): void { + this.expandedOperationId = this.expandedOperationId === operationId ? null : operationId; + } + + deleteOperation(operation: OperationResponse): void { + this.operationDeleted.emit(operation); + } + + startEdit(operation: OperationResponse): void { + this.editingOperationId = operation.id; + this.editingOperation = { + id: operation.id, + description: operation.description, + amount: operation.amount.value, + currencyCode: operation.amount.currencyCode, + tags: [...operation.tags], + attributes: { ...operation.attributes || {} } + }; + } + + cancelEdit(): void { + this.editingOperationId = null; + this.editingOperation = null; + } + + saveEdit(operation: OperationResponse): void { + if (!this.editingOperation) return; + + const updatedOperation: OperationResponse = { + ...operation, + description: this.editingOperation.description, + amount: { + value: this.editingOperation.amount, + currencyCode: this.editingOperation.currencyCode + }, + tags: this.editingOperation.tags, + attributes: this.editingOperation.attributes + }; + + this.operationUpdated.emit(updatedOperation); + this.editingOperationId = null; + this.editingOperation = null; + } + + isEditing(operationId: string): boolean { + return this.editingOperationId === operationId; + } + + addTag(): void { + if (this.editingOperation) { + this.editingOperation.tags.push(''); + } + } + + removeTag(index: number): void { + if (this.editingOperation) { + this.editingOperation.tags.splice(index, 1); + } + } + + trackByIndex(index: number): number { + return index; + } + + addAttribute(): void { + if (this.editingOperation) { + const key = `key${Object.keys(this.editingOperation.attributes).length + 1}`; + this.editingOperation.attributes[key] = ''; + } + } + + removeAttribute(key: string): void { + if (this.editingOperation) { + delete this.editingOperation.attributes[key]; + } + } + + updateAttributeKey(oldKey: string, newKey: string): void { + if (this.editingOperation && oldKey !== newKey && newKey) { + const value = this.editingOperation.attributes[oldKey]; + delete this.editingOperation.attributes[oldKey]; + this.editingOperation.attributes[newKey] = value; + } + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.html new file mode 100644 index 00000000..9a0fbdbe --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.html @@ -0,0 +1,72 @@ +
+ +
+

Retag Operations

+ + + +
+ +
+
+ +
+ + @if (retagResult) { + + } +
+ + @if (retagResult) { +
+

Retagging Results

+
+ @if (retagResult.successes.length > 0) { +
+ Successes: + {{ retagResult.successes.length }} +
+ } + @if (retagResult.errors.length > 0) { +
+ Errors: + {{ retagResult.errors.length }} +
+ } +
+ + + +
+ } +
+
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.less new file mode 100644 index 00000000..7728ce7a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.less @@ -0,0 +1,71 @@ +@import '../shared/styles/mixins.less'; + +.retag-container { + .page-container(); +} + +.retag-card { + .card-base(); +} + +.info-warning { + margin: 1.5rem 0; + padding: 1.5rem; + background: var(--tui-info-bg); + border: 2px solid var(--tui-info-fill); + border-radius: 0.5rem; + + h3 { + margin: 0 0 1rem 0; + color: var(--tui-info-fill); + font-size: 1.125rem; + } + + p { + margin: 0.5rem 0; + color: var(--tui-text-primary); + line-height: 1.5; + } + + ul { + margin: 0.75rem 0; + padding-left: 1.5rem; + + li { + margin: 0.5rem 0; + line-height: 1.5; + + strong { + font-weight: 600; + } + } + } +} + +.retag-options { + margin: 1.5rem 0; + padding: 1rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; + + .from-scratch-checkbox { + font-size: 1rem; + font-weight: 500; + } +} + +.action-buttons { + display: flex; + gap: 1rem; + margin: 2rem 0; + flex-wrap: wrap; +} + +.retag-result { + .result-section-base(); +} + +.result-stats { + .result-stats-base(); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.ts new file mode 100644 index 00000000..d3e2c9ad --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/retag-operations/retag-operations.component.ts @@ -0,0 +1,143 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { OperationsApiService } from '../operations-api.service'; +import { BudgetApiService } from '../../budget/budget-api.service'; +import { + TuiButton, + TuiLoader, + TuiTitle, + TuiLabel +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { TuiCheckbox } from '@taiga-ui/kit'; +import { FormsModule } from '@angular/forms'; +import { NotificationService } from '../shared/notification.service'; +import { CriteriaFilterComponent } from '../shared/components/criteria-filter/criteria-filter.component'; +import { OperationResultComponent } from '../shared/components/operation-result/operation-result.component'; +import { CriteriaExample } from '../shared/models/example.interface'; +import { OperationResult } from '../shared/models/result.interface'; + +@Component({ + selector: 'app-retag-operations', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TuiButton, + TuiLoader, + TuiCardLarge, + TuiTitle, + TuiLabel, + TuiCheckbox, + CriteriaFilterComponent, + OperationResultComponent + ], + templateUrl: './retag-operations.component.html', + styleUrls: ['./retag-operations.component.less'] +}) +export class RetagOperationsComponent implements OnInit { + budgetId!: string; + budgetVersion!: string; + isLoading = false; + retagResult: OperationResult | null = null; + currentCriteria = 'o => true'; + fromScratch = false; + + criteriaExamples: CriteriaExample[] = [ + { label: 'All operations:', code: 'o => true' }, + { label: 'Specific year:', code: 'o => o.Timestamp.Year == 2023' }, + { label: 'Contains text:', code: 'o => o.Description.Contains("groceries")' }, + { label: 'Missing tags:', code: 'o => o.Tags.Count == 0' }, + { label: 'Date range:', code: 'o => o.Timestamp >= DateTime.Parse("2023-01-01") && o.Timestamp < DateTime.Parse("2024-01-01")' }, + { label: 'By amount range:', code: 'o => o.Amount.Amount >= -1000 && o.Amount.Amount <= -100' }, + { label: 'Specific attribute:', code: 'o => o.Attributes.ContainsKey("category") && o.Attributes["category"] == "food"' } + ]; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private budgetApi: BudgetApiService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + this.loadBudgetVersion(); + } + + loadBudgetVersion(): void { + this.budgetApi.getAllBudgets().subscribe({ + next: (budgets: any) => { + const budget = budgets.find((b: any) => b.id === this.budgetId); + if (budget) { + this.budgetVersion = budget.version; + } + }, + error: (error: any) => { + const errorMessage = this.notificationService.handleError(error, 'Failed to load budget'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + retagOperations(criteria: string): void { + this.currentCriteria = criteria; + const action = this.fromScratch ? 'retag from scratch' : 'retag'; + const confirmMessage = `Are you sure you want to ${action} all operations matching the criteria:\n\n${criteria}\n\n${this.fromScratch ? 'This will remove all existing tags and apply tagging criteria from the beginning.' : 'This will apply tagging criteria to operations that match.'}`; + + const confirmed = confirm(confirmMessage); + if (!confirmed) return; + + if (!this.budgetVersion) { + this.notificationService.showError('Budget version not loaded. Please try again.').subscribe(); + return; + } + + this.isLoading = true; + this.retagResult = null; + + const request = { + budgetVersion: this.budgetVersion, + criteria: criteria, + fromScratch: this.fromScratch + }; + + this.operationsApi.retagOperations(this.budgetId, request).subscribe({ + next: (result) => { + this.isLoading = false; + this.retagResult = { + errors: result.errors, + successes: result.successes + }; + + if (result.errors.length === 0) { + this.notificationService.showSuccess('Operations retagged successfully').subscribe(); + this.operationsApi.triggerRefresh(this.budgetId); + // Reload budget version after successful retag + this.loadBudgetVersion(); + } else { + const errorMessage = result.errors.length > 5 + ? `Retagging completed with ${result.errors.length} errors. Check the results below.` + : `Retagging completed with errors. See details below.`; + this.notificationService.showError(errorMessage).subscribe(); + } + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to retag operations'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + viewOperations(): void { + this.router.navigate(['/budget', this.budgetId]); + } + + resetResult(): void { + this.retagResult = null; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.html new file mode 100644 index 00000000..15ff9760 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.html @@ -0,0 +1,40 @@ +
+
+ + + + +
+ + @if (examples.length > 0) { + + + } + +
+ + + +
+
diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.less new file mode 100644 index 00000000..32033d0b --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.less @@ -0,0 +1,20 @@ +@import '../../styles/mixins.less'; + +.criteria-filter { + .filters-section-base(); +} + +.filter-field { + margin-bottom: 1rem; +} + +.filter-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1rem; + margin-top: 1.5rem; +} +.filter-actions > * { + width: max-content; +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.ts new file mode 100644 index 00000000..1d5317bf --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/criteria-filter/criteria-filter.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, OnInit, OnChanges, SimpleChanges, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { TuiButton, TuiTextfield, TuiLabel } from '@taiga-ui/core'; +import { TuiTextarea } from '@taiga-ui/kit'; +import { CriteriaExample } from '../../models/example.interface'; +import { ExamplesSectionComponent } from '../examples-section/examples-section.component'; +import { CtrlEnterDirective } from '../../directives/ctrl-enter.directive'; + +@Component({ + selector: 'app-criteria-filter', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TuiButton, + TuiTextfield, + TuiLabel, + TuiTextarea, + ExamplesSectionComponent, + CtrlEnterDirective + ], + templateUrl: './criteria-filter.component.html', + styleUrls: ['./criteria-filter.component.less'] +}) +export class CriteriaFilterComponent implements OnInit, OnChanges { + @Input() initialCriteria = 'o => true'; + @Input() examples: CriteriaExample[] = []; + @Input() showExamplesInitially = false; + @Output() criteriaSubmitted = new EventEmitter(); + @Output() criteriaCleared = new EventEmitter(); + + filterForm!: FormGroup; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.filterForm = this.fb.group({ + criteria: [this.initialCriteria] + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['initialCriteria'] && this.filterForm && !changes['initialCriteria'].firstChange) { + this.filterForm.patchValue({ criteria: this.initialCriteria }); + } + } + + apply(): void { + const criteria = this.filterForm.value.criteria; + this.criteriaSubmitted.emit(criteria); + } + + clear(): void { + this.filterForm.patchValue({ criteria: this.initialCriteria }); + this.criteriaCleared.emit(); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.html new file mode 100644 index 00000000..36b09e05 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.html @@ -0,0 +1,25 @@ +
+
+

{{ title }}

+ +
+ + @if (expanded) { +
+ @for (example of examples; track example.code) { +
+ {{ example.label }} + {{ example.code }} +
+ } +
+ } +
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.less new file mode 100644 index 00000000..c27e301c --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.less @@ -0,0 +1,6 @@ +@import '../../styles/mixins.less'; + +.examples-section { + .examples-section-base(); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.ts new file mode 100644 index 00000000..f923db29 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/examples-section/examples-section.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TuiButton } from '@taiga-ui/core'; +import { CriteriaExample } from '../../models/example.interface'; + +@Component({ + selector: 'app-examples-section', + standalone: true, + imports: [CommonModule, TuiButton], + templateUrl: './examples-section.component.html', + styleUrls: ['./examples-section.component.less'] +}) +export class ExamplesSectionComponent { + @Input() examples: CriteriaExample[] = []; + @Input() title = 'Common Examples'; + @Input() expanded = false; + + toggle(): void { + this.expanded = !this.expanded; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.html new file mode 100644 index 00000000..d3475fed --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.html @@ -0,0 +1,12 @@ +@if (metadata && (metadata | objectKeys).length > 0) { + +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.less new file mode 100644 index 00000000..904f772d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.less @@ -0,0 +1,6 @@ +@import '../../styles/mixins.less'; + +.metadata { + .metadata-base(); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.ts new file mode 100644 index 00000000..619f8788 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/metadata-display/metadata-display.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ObjectKeysPipe } from '../../pipes/object-keys.pipe'; + +@Component({ + selector: 'app-metadata-display', + standalone: true, + imports: [CommonModule, ObjectKeysPipe], + templateUrl: './metadata-display.component.html', + styleUrls: ['./metadata-display.component.less'] +}) +export class MetadataDisplayComponent { + @Input() metadata: Record | null | undefined = null; + @Input() title = 'Details'; +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.html new file mode 100644 index 00000000..fa27147d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.html @@ -0,0 +1,60 @@ + +@if (successes.length > 0) { +
+
+

Success Messages ({{ successes.length }})

+ +
+ @if (showSuccesses) { + @for (success of successes; track $index) { +
+
{{ success.message || 'Success' }}
+ +
+ } + } +
+} + + +@if (errors.length > 0) { +
+
+

Errors ({{ errors.length }})

+ +
+ @if (showErrors) { + @for (error of errors; track $index) { +
+
{{ error.message || 'Error' }}
+ + @if (error.reasons && error.reasons.length > 0) { +
+ Caused by: +
    + @for (reason of error.reasons; track $index) { +
  • {{ reason.message || 'Unspecified reason' }}
  • + } +
+
+ } +
+ } + } +
+} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.less new file mode 100644 index 00000000..f484b396 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.less @@ -0,0 +1,34 @@ +@import '../../styles/mixins.less'; + +.section-header-with-toggle { + .section-header-with-toggle(); +} + +.successes-section { + margin-top: 1.5rem; + + h4 { + color: var(--tui-success-fill); + } +} + +.success-item { + .success-item-base(); +} + +.errors-section { + margin-top: 1.5rem; + + h4 { + color: var(--tui-error-fill); + } +} + +.error-item { + .error-item-base(); +} + +.nested-reasons { + .nested-reasons-base(); +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.ts new file mode 100644 index 00000000..e2fe5697 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/components/operation-result/operation-result.component.ts @@ -0,0 +1,29 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TuiButton } from '@taiga-ui/core'; +import { IError, ISuccess } from '../../../../budget/models'; +import { MetadataDisplayComponent } from '../metadata-display/metadata-display.component'; + +@Component({ + selector: 'app-operation-result', + standalone: true, + imports: [CommonModule, TuiButton, MetadataDisplayComponent], + templateUrl: './operation-result.component.html', + styleUrls: ['./operation-result.component.less'] +}) +export class OperationResultComponent { + @Input() successes: ISuccess[] = []; + @Input() errors: IError[] = []; + + showSuccesses = true; + showErrors = true; + + toggleSuccesses(): void { + this.showSuccesses = !this.showSuccesses; + } + + toggleErrors(): void { + this.showErrors = !this.showErrors; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/directives/ctrl-enter.directive.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/directives/ctrl-enter.directive.ts new file mode 100644 index 00000000..0be02046 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/directives/ctrl-enter.directive.ts @@ -0,0 +1,18 @@ +import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; + +@Directive({ + selector: '[appCtrlEnter]', + standalone: true +}) +export class CtrlEnterDirective { + @Output() appCtrlEnter = new EventEmitter(); + + @HostListener('keydown', ['$event']) + onKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter' && event.ctrlKey) { + event.preventDefault(); + this.appCtrlEnter.emit(); + } + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/example.interface.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/example.interface.ts new file mode 100644 index 00000000..2ab0120e --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/example.interface.ts @@ -0,0 +1,5 @@ +export interface CriteriaExample { + label: string; + code: string; +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/result.interface.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/result.interface.ts new file mode 100644 index 00000000..ab50580a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/models/result.interface.ts @@ -0,0 +1,13 @@ +import { IError, ISuccess } from '../../../budget/models'; + +export interface OperationResult { + errors: IError[]; + successes: ISuccess[]; +} + +export interface ImportResult extends OperationResult { + registered: number; + duplicates: number; + duplicatesList?: any[]; +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/notification.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/notification.service.ts new file mode 100644 index 00000000..a7d0239d --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/notification.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { TuiDialogService } from '@taiga-ui/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationService { + constructor(private dialogService: TuiDialogService) {} + + showError(message: string): Observable { + return this.dialogService.open(message, { + label: 'Error', + size: 'm', + closeable: true, + dismissible: true + }); + } + + showSuccess(message: string): Observable { + return this.dialogService.open(message, { + label: 'Success', + size: 's', + closeable: true, + dismissible: true + }); + } + + showWarning(message: string): Observable { + return this.dialogService.open(message, { + label: 'Warning', + size: 'm', + closeable: true, + dismissible: true + }); + } + + confirm(message: string, title: string = 'Confirm'): Observable { + return new Observable(observer => { + this.dialogService.open(message, { + label: title, + size: 'm', + closeable: true, + dismissible: true + }).subscribe({ + next: () => { + observer.next(true); + observer.complete(); + }, + error: () => { + observer.next(false); + observer.complete(); + } + }); + }); + } + + handleError(error: any, defaultMessage: string): string { + let errorMessage = defaultMessage; + + if (error.status === 400 && Array.isArray(error.error)) { + const errors = error.error as any[]; + errorMessage = errors.map(e => e.message || e).join('; '); + } else if (error.error?.message) { + errorMessage = error.error.message; + } + + return errorMessage; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/operations-helper.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/operations-helper.service.ts new file mode 100644 index 00000000..f8bde58f --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/operations-helper.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Observable, switchMap, of, throwError } from 'rxjs'; +import { OperationsApiService } from '../operations-api.service'; +import { BudgetApiService } from '../../budget/budget-api.service'; +import { OperationResponse, BudgetResponse, UpdateResultResponse } from '../../budget/models'; + +@Injectable({ + providedIn: 'root' +}) +export class OperationsHelperService { + constructor( + private operationsApi: OperationsApiService, + private budgetApi: BudgetApiService + ) {} + + /** + * Delete a single operation by ID + */ + deleteOperation(budgetId: string, operationId: string): Observable { + const criteria = `o => o.Id == Guid.Parse("${operationId}")`; + return this.operationsApi.removeOperations(budgetId, { criteria }); + } + + /** + * Update a single operation + */ + updateOperation(budgetId: string, operation: OperationResponse): Observable { + return this.budgetApi.getAllBudgets().pipe( + switchMap((budgetList: BudgetResponse[]) => { + const budget = budgetList.find((b: BudgetResponse) => b.id === budgetId); + + if (!budget) { + return throwError(() => new Error('Budget not found')); + } + + const request = { + operations: [operation], + budgetVersion: budget.version, + transferConfidenceLevel: undefined, + taggingMode: 'Skip' + }; + + return this.operationsApi.updateOperations(budgetId, request); + }) + ); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/currency-format.pipe.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/currency-format.pipe.ts new file mode 100644 index 00000000..d4921089 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/currency-format.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'currencyFormat', + standalone: true +}) +export class CurrencyFormatPipe implements PipeTransform { + transform(amount: number, currencyCode: string | null = null): string { + // Format number with space as thousand separator + const formattedAmount = amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + return currencyCode ? `${formattedAmount} ${currencyCode}` : formattedAmount; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/date-format.pipe.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/date-format.pipe.ts new file mode 100644 index 00000000..7cfb5459 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/date-format.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'dateFormat', + standalone: true +}) +export class DateFormatPipe implements PipeTransform { + transform(timestamp: string): string { + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}.${month}.${day}, ${hours}:${minutes}:${seconds}`; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/object-keys.pipe.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/object-keys.pipe.ts new file mode 100644 index 00000000..51bff463 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/pipes/object-keys.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'objectKeys', + standalone: true +}) +export class ObjectKeysPipe implements PipeTransform { + transform(obj: Record | null | undefined): string[] { + return obj ? Object.keys(obj) : []; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/styles/mixins.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/styles/mixins.less new file mode 100644 index 00000000..b3752392 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/shared/styles/mixins.less @@ -0,0 +1,252 @@ +// Custom scrollbar mixin +.custom-scrollbar() { + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--tui-background-neutral-2); + border-radius: 2px; + } +} + +// Page container mixin +.page-container() { + padding: 2rem; + margin: 0 auto; +} + +// Card styling +.card-base() { + padding: 2rem; +} + +// Header with actions +.header-with-actions() { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + + .header-actions { + display: flex; + gap: 1rem; + } +} + +// Filter section +.filters-section-base() { + margin-bottom: 1.5rem; + padding: 1.5rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; + + h3 { + margin-bottom: 1rem; + } +} + +// Examples section +.examples-section-base() { + margin: 1rem 0; + padding: 1rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + + .examples-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + + h4 { + margin: 0; + color: var(--tui-text-secondary); + } + } + + .examples-list { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .example-item { + padding: 0.75rem; + background: var(--tui-background-neutral-1); + border-radius: 0.25rem; + + strong { + display: block; + margin-bottom: 0.25rem; + color: var(--tui-text-secondary); + font-size: 0.875rem; + } + + code { + display: block; + background: var(--tui-background-neutral-2); + padding: 0.5rem; + border-radius: 0.25rem; + font-family: monospace; + font-size: 0.875rem; + color: var(--tui-text-primary); + } + } +} + +// Section header with toggle +.section-header-with-toggle() { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + + h4 { + margin: 0; + } +} + +// Result display +.result-section-base() { + margin-top: 2rem; + padding: 1.5rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; +} + +// Result stats +.result-stats-base() { + display: flex; + gap: 2rem; + margin: 1rem 0; + flex-wrap: wrap; + + .stat-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .stat-label { + font-size: 0.875rem; + color: var(--tui-text-secondary); + } + + .stat-value { + font-size: 1.5rem; + font-weight: 700; + } + + &.success .stat-value { + color: var(--tui-success-fill); + } + + &.warning .stat-value { + color: var(--tui-warning-fill); + } + + &.error .stat-value { + color: var(--tui-error-fill); + } + + &.info .stat-value { + color: var(--tui-primary); + } + } +} + +// Success/Error items +.success-item-base() { + margin-bottom: 1rem; + padding: 1rem; + background: var(--tui-success-bg); + border-left: 3px solid var(--tui-success-fill); + border-radius: 0.25rem; + + .success-message { + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--tui-text-primary); + } +} + +.error-item-base() { + margin-bottom: 1rem; + padding: 1rem; + background: var(--tui-error-bg); + border-left: 3px solid var(--tui-error-fill); + border-radius: 0.25rem; + + .error-message { + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--tui-text-primary); + } +} + +// Metadata display +.metadata-base() { + margin-top: 0.75rem; + padding: 0.75rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + font-size: 0.875rem; + + strong { + display: block; + margin-bottom: 0.5rem; + color: var(--tui-text-secondary); + } + + .metadata-item { + display: flex; + gap: 0.5rem; + margin: 0.25rem 0; + + .metadata-key { + font-weight: 600; + color: var(--tui-text-secondary); + min-width: 120px; + } + + .metadata-value { + color: var(--tui-text-primary); + font-family: monospace; + } + } +} + +// Nested reasons +.nested-reasons-base() { + margin-top: 0.75rem; + padding: 0.75rem; + background: var(--tui-background-base); + border-radius: 0.25rem; + font-size: 0.875rem; + + strong { + display: block; + margin-bottom: 0.5rem; + color: var(--tui-text-secondary); + } + + ul { + margin: 0; + padding-left: 1.5rem; + + li { + margin: 0.25rem 0; + } + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.html new file mode 100644 index 00000000..cbdc3aef --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.html @@ -0,0 +1,227 @@ +
+
+
+

Transfers

+
+ + + +
+
+ + + @if (showRegisterForm) { +
+

Register New Transfer

+
+
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+
+ +
+ + +
+
+ } + + +
+

Filters

+
+
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ +
+
+
+ + +
+ @if (transfers$ | async; as transfersList) { + + @if (transfersList.recorded && transfersList.recorded.length > 0) { +
+

Recorded Transfers ({{ transfersList.recorded.length }})

+ + +
+ } + + + @if (transfersList.unregistered && transfersList.unregistered.length > 0) { +
+

Unregistered Transfers ({{ transfersList.unregistered.length }})

+ + +
+ } + + @if ((!transfersList.recorded || transfersList.recorded.length === 0) && + (!transfersList.unregistered || transfersList.unregistered.length === 0)) { +
+

No transfers found.

+ +
+ } @else { +
+ Total: {{ transfersList.recorded.length + transfersList.unregistered.length }} transfer(s) + ({{ transfersList.recorded.length }} recorded, {{ transfersList.unregistered.length }} unregistered) +
+ } + } @else { + +
Loading transfers...
+
+ } +
+
+
diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.less new file mode 100644 index 00000000..b9074067 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.less @@ -0,0 +1,133 @@ +@import '../shared/styles/mixins.less'; + +.transfers-container { + padding: 1.5rem; +} + +.transfers-card { + .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + + .header-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + } + } + + .register-form-section { + margin-bottom: 2rem; + padding: 1.5rem; + background: var(--tui-background-neutral-1); + border-radius: 0.5rem; + border: 1px solid var(--tui-border-normal); + + h3 { + margin-bottom: 1rem; + } + + .form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; + } + + .form-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .select-input { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--tui-border-normal); + border-radius: 0.25rem; + background: var(--tui-background-base); + font-size: 0.875rem; + } + } + + .form-actions { + display: flex; + gap: 0.75rem; + margin-top: 1rem; + } + } + + .filters-section { + margin-bottom: 1.5rem; + + h3 { + margin-bottom: 1rem; + } + + .filters-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + align-items: end; + + .filter-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .select-input { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--tui-border-normal); + border-radius: 0.25rem; + background: var(--tui-background-base); + font-size: 0.875rem; + } + + &.filter-actions { + display: flex; + justify-content: flex-end; + } + } + } + } + + .transfers-section { + .transfers-group { + margin-bottom: 2rem; + + h3 { + margin-bottom: 1rem; + } + } + + .transfers-count { + margin-top: 1rem; + padding: 0.75rem; + text-align: center; + color: var(--tui-text-secondary); + font-size: 0.875rem; + } + + .empty-state { + text-align: center; + padding: 3rem 1rem; + color: var(--tui-text-secondary); + + p { + margin-bottom: 1rem; + font-size: 1rem; + } + } + + .loading-placeholder { + padding: 2rem; + text-align: center; + color: var(--tui-text-secondary); + } + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.ts new file mode 100644 index 00000000..8a07ef26 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-list/transfers-list.component.ts @@ -0,0 +1,224 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, catchError, of } from 'rxjs'; +import { OperationsApiService } from '../operations-api.service'; +import { TransferResponse, TransfersListResponse, RegisterTransferRequest } from '../../budget/models'; +import { + TuiButton, + TuiLoader, + TuiTitle, + TuiTextfield, + TuiLabel +} from '@taiga-ui/core'; +import { TuiCardLarge } from '@taiga-ui/layout'; +import { TransfersTableComponent } from '../transfers-table/transfers-table.component'; +import { NotificationService } from '../shared/notification.service'; + +@Component({ + selector: 'app-transfers-list', + standalone: true, + imports: [ + CommonModule, + FormsModule, + TuiButton, + TuiLoader, + TuiTextfield, + TuiLabel, + TuiCardLarge, + TuiTitle, + TransfersTableComponent + ], + templateUrl: './transfers-list.component.html', + styleUrls: ['./transfers-list.component.less'] +}) +export class TransfersListComponent implements OnInit { + budgetId!: string; + transfers$!: Observable; + isLoading = false; + + fromDate: string = ''; + tillDate: string = ''; + accuracy = ''; + + // Registration form + showRegisterForm = false; + newTransfer: RegisterTransferRequest = { + sourceId: '', + sinkId: '', + comment: '', + accuracy: 'Likely' + }; + + accuracyOptions = ['Likely', 'Exact']; + + constructor( + private route: ActivatedRoute, + private router: Router, + private operationsApi: OperationsApiService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.budgetId = this.route.snapshot.params['budgetId']; + // Set default dates: last month to now + const now = new Date(); + this.tillDate = this.formatDateForInput(now); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); + this.fromDate = this.formatDateForInput(lastMonth); + this.loadTransfers(); + } + + private formatDateForInput(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + private parseDateInput(dateString: string): Date | null { + if (!dateString) return null; + const date = new Date(dateString); + return isNaN(date.getTime()) ? null : date; + } + + loadTransfers(): void { + const from = this.parseDateInput(this.fromDate); + const till = this.parseDateInput(this.tillDate); + + this.transfers$ = this.operationsApi.searchTransfers( + this.budgetId, + from || undefined, + till || undefined, + this.accuracy || undefined + ).pipe( + catchError(error => { + const errorMessage = this.notificationService.handleError(error, 'Failed to load transfers'); + this.notificationService.showError(errorMessage).subscribe(); + return of({ recorded: [], unregistered: [] }); + }) + ); + } + + onDateChange(): void { + this.loadTransfers(); + } + + onAccuracyChange(): void { + this.loadTransfers(); + } + + resetFilters(): void { + const now = new Date(); + this.tillDate = this.formatDateForInput(now); + const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()); + this.fromDate = this.formatDateForInput(lastMonth); + this.accuracy = ''; + this.loadTransfers(); + } + + toggleRegisterForm(): void { + this.showRegisterForm = !this.showRegisterForm; + if (!this.showRegisterForm) { + this.resetNewTransfer(); + } + } + + resetNewTransfer(): void { + this.newTransfer = { + sourceId: '', + sinkId: '', + comment: '', + accuracy: 'Likely' + }; + } + + registerTransfer(): void { + if (!this.newTransfer.sourceId || !this.newTransfer.sinkId || !this.newTransfer.comment) { + this.notificationService.showError('Please fill in all required fields').subscribe(); + return; + } + + this.isLoading = true; + + this.operationsApi.registerTransfers(this.budgetId, { + transfers: [this.newTransfer] + }).subscribe({ + next: () => { + this.isLoading = false; + this.notificationService.showSuccess('Transfer registered successfully').subscribe(); + this.resetNewTransfer(); + this.showRegisterForm = false; + this.loadTransfers(); + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to register transfer'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + onDeleteTransfer(transfer: TransferResponse): void { + const confirmMessage = `Are you sure you want to delete this transfer?\n\nSource: ${transfer.source.description}\nSink: ${transfer.sink.description}\nFee: ${transfer.fee.value} ${transfer.fee.currencyCode}\n\nThis action cannot be undone.`; + + if (!confirm(confirmMessage)) { + return; + } + + this.isLoading = true; + + this.operationsApi.removeTransfers(this.budgetId, { + sourceIds: [transfer.sourceId], + all: false + }).subscribe({ + next: () => { + this.isLoading = false; + this.notificationService.showSuccess('Transfer deleted successfully').subscribe(); + this.loadTransfers(); + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to delete transfer'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + quickRegisterTransfer(transfer: TransferResponse): void { + this.isLoading = true; + + const request: RegisterTransferRequest = { + sourceId: transfer.sourceId, + sinkId: transfer.sinkId, + comment: transfer.comment, + accuracy: transfer.accuracy, + fee: transfer.fee + }; + + this.operationsApi.registerTransfers(this.budgetId, { + transfers: [request] + }).subscribe({ + next: () => { + this.isLoading = false; + this.notificationService.showSuccess('Transfer registered successfully').subscribe(); + this.loadTransfers(); + }, + error: (error) => { + this.isLoading = false; + const errorMessage = this.notificationService.handleError(error, 'Failed to register transfer'); + this.notificationService.showError(errorMessage).subscribe(); + } + }); + } + + navigateToOperations(): void { + this.router.navigate(['/budget', this.budgetId, 'operations']); + } + + navigateToBudget(): void { + this.router.navigate(['/budget', this.budgetId, 'details']); + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.html new file mode 100644 index 00000000..d3a8fb46 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.html @@ -0,0 +1,105 @@ +
+ + + + + + + + + @if (showActions) { + + } + + + + @for (transfer of transfers; track transfer.sourceId) { + + + + + + + @if (showActions) { + + } + + @if (expandedTransferId === transfer.sourceId) { + + + + } + } @empty { + + + + } + +
SourceSinkFeeCommentAccuracyActions
+
+
+ {{ transfer.source.amount.value | currencyFormat: transfer.source.amount.currencyCode }} +
+
{{ transfer.source.description }}
+
{{ transfer.source.timestamp | dateFormat }}
+
+
+
+
+ {{ transfer.sink.amount.value | currencyFormat: transfer.sink.amount.currencyCode }} +
+
{{ transfer.sink.description }}
+
{{ transfer.sink.timestamp | dateFormat }}
+
+
+ {{ transfer.fee.value | currencyFormat: transfer.fee.currencyCode }} + {{ transfer.comment }} + {{ transfer.accuracy }} + +
+ + @if (showQuickRegister) { + + } + +
+
+
+
+

Operations in Transfer

+ + +
+
+
+ No transfers found. +
+
+ diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.less new file mode 100644 index 00000000..af4dcea8 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.less @@ -0,0 +1,154 @@ +@import '../shared/styles/mixins.less'; + +.transfers-table-container { + overflow-x: auto; + border-radius: 0.5rem; + border: 1px solid var(--tui-border-normal); +} + +.transfers-table { + width: 100%; + border-collapse: collapse; + background: var(--tui-background-base); + + thead { + background: var(--tui-background-neutral-1); + position: sticky; + top: 0; + z-index: 1; + + tr { + border-bottom: 2px solid var(--tui-border-normal); + } + + th { + padding: 0.75rem 1rem; + text-align: left; + font-weight: 600; + font-size: 0.875rem; + color: var(--tui-text-secondary); + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + + &.col-source, + &.col-sink { + width: 250px; + } + + &.col-fee { + width: 120px; + text-align: right; + } + + &.col-comment { + width: auto; + min-width: 200px; + } + + &.col-accuracy { + width: 100px; + } + + &.col-actions { + width: 120px; + } + } + } + + tbody { + tr { + border-bottom: 1px solid var(--tui-border-normal); + + &:hover { + background: var(--tui-background-neutral-1); + } + + &.transfer-details-row { + background: var(--tui-background-neutral-2); + + td { + padding: 1rem; + } + } + } + + td { + padding: 0.75rem 1rem; + vertical-align: top; + border-right: 1px solid var(--tui-border-normal); + + &:last-child { + border-right: none; + } + + &.empty-state { + text-align: center; + padding: 2rem; + color: var(--tui-text-secondary); + } + } + } +} + +.operation-info { + .operation-amount { + font-weight: 600; + font-size: 0.9375rem; + + &.positive { + color: var(--tui-success-fill); + } + + &.negative { + color: var(--tui-error-fill); + } + } + + .operation-description { + font-size: 0.875rem; + margin-top: 0.25rem; + color: var(--tui-text-primary); + } + + .operation-date { + font-size: 0.75rem; + color: var(--tui-text-secondary); + margin-top: 0.25rem; + } +} + +.col-fee { + text-align: right; + font-weight: 600; + + &.positive { + color: var(--tui-success-fill); + } + + &.negative { + color: var(--tui-error-fill); + } +} + +.transfer-details { + padding: 1rem; + + .operations-section { + h4 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--tui-text-primary); + } + } +} + +.action-buttons { + display: flex; + gap: 0.5rem; + align-items: center; +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.ts new file mode 100644 index 00000000..816e1774 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/operations/transfers-table/transfers-table.component.ts @@ -0,0 +1,54 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TuiButton, TuiExpand } from '@taiga-ui/core'; +import { TuiChip } from '@taiga-ui/kit'; +import { TransferResponse } from '../../budget/models'; +import { CurrencyFormatPipe } from '../shared/pipes/currency-format.pipe'; +import { DateFormatPipe } from '../shared/pipes/date-format.pipe'; +import { OperationsTableComponent } from '../operations-table/operations-table.component'; + +@Component({ + selector: 'app-transfers-table', + standalone: true, + imports: [ + CommonModule, + TuiButton, + TuiExpand, + TuiChip, + CurrencyFormatPipe, + DateFormatPipe, + OperationsTableComponent + ], + templateUrl: './transfers-table.component.html', + styleUrls: ['./transfers-table.component.less'] +}) +export class TransfersTableComponent { + @Input() transfers: TransferResponse[] = []; + @Input() showActions = true; + @Input() showQuickRegister = false; + @Output() transferDeleted = new EventEmitter(); + @Output() transferRegistered = new EventEmitter(); + + expandedTransferId: string | null = null; + + toggleTransferDetails(transferId: string): void { + this.expandedTransferId = this.expandedTransferId === transferId ? null : transferId; + } + + deleteTransfer(transfer: TransferResponse): void { + this.transferDeleted.emit(transfer); + } + + registerTransfer(transfer: TransferResponse): void { + this.transferRegistered.emit(transfer); + } + + getTransferOperations(transfer: TransferResponse) { + return [transfer.source, transfer.sink]; + } + + trackByIndex(index: number): number { + return index; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/theme.service.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/theme.service.ts new file mode 100644 index 00000000..67898227 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/app/theme.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + private readonly THEME_KEY = 'budget-app-theme'; + private isDarkTheme$ = new BehaviorSubject(this.getInitialTheme()); + + get isDark$(): Observable { + return this.isDarkTheme$.asObservable(); + } + + get isDark(): boolean { + return this.isDarkTheme$.value; + } + + toggleTheme(): void { + const newTheme = !this.isDarkTheme$.value; + this.isDarkTheme$.next(newTheme); + localStorage.setItem(this.THEME_KEY, newTheme ? 'dark' : 'light'); + } + + private getInitialTheme(): boolean { + const savedTheme = localStorage.getItem(this.THEME_KEY); + if (savedTheme) { + return savedTheme === 'dark'; + } + // Check system preference + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } +} + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.development.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.development.ts new file mode 100644 index 00000000..d9d9af83 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +// Runtime configuration is loaded from /api/config endpoint +// This file is kept for backwards compatibility +export const environment = { + production: false +}; diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.ts new file mode 100644 index 00000000..582ae58e --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/environments/environment.ts @@ -0,0 +1,5 @@ +// Runtime configuration is loaded from /api/config endpoint +// This file is kept for backwards compatibility but apiUrl is not used +export const environment = { + production: true +}; diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/index.html b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/index.html new file mode 100644 index 00000000..1bb66b09 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/index.html @@ -0,0 +1,13 @@ + + + + + BudgetClient + + + + + + + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/main.ts b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/main.ts new file mode 100644 index 00000000..35b00f34 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/styles.less b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/styles.less new file mode 100644 index 00000000..3e87c958 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/src/styles.less @@ -0,0 +1,2 @@ +@import 'node_modules/@taiga-ui/core/styles/taiga-ui-theme.less'; +@import 'node_modules/@taiga-ui/core/styles/taiga-ui-fonts.less'; \ No newline at end of file diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.app.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.app.json new file mode 100644 index 00000000..3775b37e --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.json new file mode 100644 index 00000000..5525117c --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.spec.json b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.spec.json new file mode 100644 index 00000000..5fb748d9 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Client/budget-client/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Server/Dockerfile b/src/Hosts/NVs.Budget.Hosts.Web.Server/Dockerfile new file mode 100644 index 00000000..9f605a8a --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +USER $APP_UID +WORKDIR /app +EXPOSE 7237 +EXPOSE 5153 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY . . +WORKDIR /src/src/Hosts/NVs.Budget.Hosts.Web.Server +RUN dotnet restore NVs.Budget.Hosts.Web.Server.csproj +RUN dotnet build NVs.Budget.Hosts.Web.Server.csproj -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish NVs.Budget.Hosts.Web.Server.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS preps-development + + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "NVs.Budget.Hosts.Web.Server.dll", "--launch-profile", "https"] diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Server/NVs.Budget.Hosts.Web.Server.csproj b/src/Hosts/NVs.Budget.Hosts.Web.Server/NVs.Budget.Hosts.Web.Server.csproj new file mode 100644 index 00000000..a06be1d2 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/NVs.Budget.Hosts.Web.Server.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + Linux + a456c0d1-4c8b-44a5-95c9-27dd13084fbf + + + + + .dockerignore + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Server/Program.cs b/src/Hosts/NVs.Budget.Hosts.Web.Server/Program.cs new file mode 100644 index 00000000..731c1b45 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/Program.cs @@ -0,0 +1,73 @@ +using NVs.Budget.Application; +using NVs.Budget.Application.Contracts.Entities; +using NVs.Budget.Application.Contracts.Services; +using NVs.Budget.Application.UseCases; +using NVs.Budget.Controllers.Web; +using NVs.Budget.Infrastructure.ExchangeRates.CBRF; +using NVs.Budget.Infrastructure.Files.CSV; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; +using NVs.Budget.Infrastructure.Persistence.EF; +using NVs.Budget.Infrastructure.Persistence.EF.Context; +using NVs.Budget.Utilities.Expressions; +using Serilog; +using Serilog.Events; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseSerilog(Log.Logger); + +builder.Services.AddLogging(b => b.AddSerilog(dispose: true)); +var identityConnectionString = builder.Configuration.GetConnectionString("IdentityContext") ?? throw new InvalidOperationException("No connection string found for IdentityContext!"); +var contentConnectionString = builder.Configuration.GetConnectionString("BudgetContext") ?? throw new InvalidOperationException("No connection string found for BudgetContext!"); +var yandexAuthConfig = builder.Configuration.GetSection("Auth:Yandex").Get() ?? throw new InvalidOperationException("No Auth config found for Yandex provider!"); +var frontendUrl = builder.Configuration.GetSection("FrontendUrl").Get() ?? throw new InvalidOperationException("No FrontendUrl config found!"); + +builder.Services + .AddEfCorePersistence( + contentConnectionString, + ReadableExpressionsParser.Default + ) + .AddYandexAuth(yandexAuthConfig, identityConnectionString) + .AddScoped() + .AddScoped(p => p.GetRequiredService().CachedUser) + .AddTransient() + .AddTransient(p => p.GetRequiredService().CreateAccountant()) + .AddTransient(p => p.GetRequiredService().CreateAccountManager()) + .AddTransient(p => p.GetRequiredService().CreateReckoner()) + .AddApplicationUseCases() + .AddSingleton(new Factory().CreateProvider()) + .AddCors(opts => + { + var allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get() ?? string.Empty; + opts.AddDefaultPolicy(b => b.WithOrigins(allowedOrigins.Split(';')).AllowCredentials().AllowAnyHeader().AllowAnyMethod()); + }) + .AddCsvFiles(contentConnectionString) + .AddSingleton(ReadableExpressionsParser.Default) + .AddWebControllers(); + +var app = builder.Build(); +app.UseSerilogRequestLogging(); +app.UseYandexAuth(frontendUrl) + .UseWebControllers(app.Environment.IsDevelopment()); + +app.UseCors(); +app.MapGet("/", () => Results.Redirect(frontendUrl)); +app.MapGet("/admin/patch-db", async (IEnumerable migrators, CancellationToken ct) => +{ + foreach (var migrator in migrators) + { + await migrator.MigrateAsync(ct); + } +}); +app.MapGet("/health", () => Results.Ok()); + +app.MapControllers(); + +app.Run(); diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Server/Properties/launchSettings.json b/src/Hosts/NVs.Budget.Hosts.Web.Server/Properties/launchSettings.json new file mode 100644 index 00000000..b81f7a53 --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://+:7237;http://+:5153", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ConnectionStrings__BudgetContext": "Host=localhost;Port=20000;Database=budgetdb;Username=postgres;Password=postgres", + "ConnectionStrings__IdentityContext": "Host=localhost;Port=20000;Database=budgetdb;Username=postgres;Password=postgres", + "ASPNETCORE_Kestrel__Certificates__Default__Path": "C:\\Users\\nvsnk\\sources\\repos\\budget\\src\\Hosts\\web-debug\\certs\\aspnetapp.pfx", + "ASPNETCORE_Kestrel__Certificates__Default__Password": "dev-password-do-not-use-in-production", + "Auth__Yandex__ClientSecret":"35b1c0f020d64cf09f2c50663c173c06", + "Auth__Yandex__ClientId":"8794694441714e78a998f06a5e390996" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5153", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.Development.json b/src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.Development.json new file mode 100644 index 00000000..fd1723be --- /dev/null +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.Development.json @@ -0,0 +1,17 @@ +{ + "ConnectionStrings": { + "BudgetContext": "User ID=postgres;Password=postgres;Host=localhost;Port=20000;Database=budgetdb;", + "IdentityContext": "User ID=postgres;Password=postgres;Host=localhost;Port=20000;Database=budgetdb;" + }, + + "AllowedHosts": "localhost", + "AllowedOrigins": "https://localhost;https://localhost:4200;https://localhost:7237", + "FrontendUrl": "https://localhost:4200", + + "Auth": { + "Yandex": { + "ClientId": "...", + "ClientSecret": "..." + } + } +} diff --git a/src/Hosts/NVs.Budget.Hosts.Console/appsettings.json b/src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.json similarity index 71% rename from src/Hosts/NVs.Budget.Hosts.Console/appsettings.json rename to src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.json index 6a403cdb..02085c58 100644 --- a/src/Hosts/NVs.Budget.Hosts.Console/appsettings.json +++ b/src/Hosts/NVs.Budget.Hosts.Web.Server/appsettings.json @@ -14,10 +14,16 @@ }, "ConnectionStrings": { - "BudgetContext": "" + "BudgetContext": "", + "IdentityContext": "" }, - "OutputOptions": { - "ShowSuccesses": false + "AllowedHosts": "*", + + "Auth": { + "Yandex": { + "ClientId": "YOUR_YANDEX_CLIENT_ID", + "ClientSecret": "YOUR_YANDEX_CLIENT_SECRET" + } } } diff --git a/src/Hosts/RELEASE-GUIDE.md b/src/Hosts/RELEASE-GUIDE.md new file mode 100644 index 00000000..49db0435 --- /dev/null +++ b/src/Hosts/RELEASE-GUIDE.md @@ -0,0 +1,220 @@ +# Docker Release Guide + +This guide explains how to use the `Release-DockerImages.ps1` script to build and publish Docker images for the Budget application. + +## Prerequisites + +- PowerShell 5.1 or later (PowerShell Core 7+ recommended) +- Docker installed and running +- Access to a Docker registry (Docker Hub, GitHub Container Registry, or private registry) + +## Quick Start + +### First Time Setup + +1. Navigate to the Hosts directory: +```powershell +cd src\Hosts +``` + +2. Run the script (it will prompt for registry configuration): +```powershell +.\Release-DockerImages.ps1 +``` + +3. When prompted, provide: + - **Docker Registry URL**: e.g., `docker.io`, `ghcr.io`, or your private registry + - **Registry Username**: Your Docker registry username + - **Registry Password**: Your Docker registry password (hidden input) + - **Image Namespace**: e.g., `mycompany/budget` or just your username + +The script will save these settings in `docker-registry-config.json` for future use. + +## Usage Examples + +### Build and Push with Default Version (latest) +```powershell +.\Release-DockerImages.ps1 +``` + +### Build and Push with Specific Version +```powershell +.\Release-DockerImages.ps1 -Version "1.2.3" +``` + +### Build Locally Without Pushing +```powershell +.\Release-DockerImages.ps1 -SkipPush +``` + +### Push Previously Built Images +```powershell +.\Release-DockerImages.ps1 -SkipBuild -Version "1.2.3" +``` + +### Reconfigure Registry Settings +```powershell +.\Release-DockerImages.ps1 -ConfigureRegistry +``` + +## Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `-Version` | Version tag for images | `latest` | +| `-SkipBuild` | Skip building images, only push existing ones | `false` | +| `-SkipPush` | Build images but don't push to registry | `false` | +| `-ConfigureRegistry` | Force reconfiguration of registry settings | `false` | + +## What the Script Does + +1. **Validates Environment**: Checks that Docker is installed and accessible +2. **Loads/Creates Configuration**: Uses saved registry config or prompts for new configuration +3. **Builds Server Image**: + - Uses `NVs.Budget.Hosts.Web.Server/Dockerfile` + - Build context: Repository root + - Tags as `budget-server:` +4. **Builds Client Image**: + - Uses `NVs.Budget.Hosts.Web.Client/Dockerfile` + - Build context: `src` directory + - Includes Angular build + - Tags as `budget-client:` +5. **Logs into Registry**: Authenticates with the configured Docker registry +6. **Pushes Images**: Tags and pushes both images to the registry + +## Registry Configuration + +The script stores registry credentials in `docker-registry-config.json`. This file contains: +- Registry URL +- Username +- Encrypted password (Windows DPAPI encrypted) +- Namespace/repository path +- Configuration date + +**⚠️ Security Note**: The password is encrypted using Windows Data Protection API (DPAPI), which means it can only be decrypted by the same user on the same machine. Do not commit this file to version control. + +## Configuration File Location + +- **Config File**: `src/Hosts/docker-registry-config.json` +- **Script Location**: `src/Hosts/Release-DockerImages.ps1` + +## Docker Registry Examples + +### Docker Hub +``` +Registry: docker.io +Username: your-dockerhub-username +Namespace: your-dockerhub-username +``` +Images will be: `docker.io/your-dockerhub-username/budget-server:latest` + +### GitHub Container Registry +``` +Registry: ghcr.io +Username: your-github-username +Namespace: your-github-username +``` +Images will be: `ghcr.io/your-github-username/budget-server:latest` + +### Private Registry +``` +Registry: registry.mycompany.com +Username: your-username +Namespace: mycompany/budget +``` +Images will be: `registry.mycompany.com/mycompany/budget/budget-server:latest` + +## Troubleshooting + +### Docker Not Found +``` +✗ Docker is not installed or not in PATH +``` +**Solution**: Install Docker Desktop or ensure Docker is in your PATH + +### Login Failed +``` +✗ Docker login failed +``` +**Solution**: +- Verify your credentials +- Check if you have access to the registry +- Run with `-ConfigureRegistry` to re-enter credentials + +### Build Failed +``` +✗ Failed to build Server/Client +``` +**Solution**: +- Check Docker logs for specific errors +- Ensure all source files are present +- Verify Dockerfile paths are correct +- Check available disk space + +### Permission Denied +``` +denied: permission denied for resource +``` +**Solution**: +- Verify you have push permissions to the registry +- Check namespace/repository exists and you have access +- For Docker Hub, the repository must exist before first push + +## CI/CD Integration + +For automated builds in CI/CD pipelines, you can: + +1. Store credentials in pipeline secrets +2. Create the config file programmatically: +```powershell +$config = @{ + Registry = $env:DOCKER_REGISTRY + Username = $env:DOCKER_USERNAME + EncryptedPassword = ConvertFrom-SecureString (ConvertTo-SecureString $env:DOCKER_PASSWORD -AsPlainText -Force) + Namespace = $env:DOCKER_NAMESPACE +} +$config | ConvertTo-Json | Set-Content "docker-registry-config.json" +``` + +3. Run the build: +```powershell +.\Release-DockerImages.ps1 -Version $env:BUILD_VERSION +``` + +## Local Development + +For local testing without pushing to registry: +```powershell +.\Release-DockerImages.ps1 -SkipPush -Version "dev" +``` + +Then run locally: +```powershell +docker run -p 5153:5153 budget-server:dev +docker run -p 8080:8080 budget-client:dev +``` + +## Version Tagging Strategy + +Consider using semantic versioning: +- **Major.Minor.Patch**: `1.2.3` for releases +- **latest**: Always points to the most recent stable release +- **dev**: Development builds +- **Branch names**: `feature-auth`, `hotfix-123` + +Example workflow: +```powershell +# Development build +.\Release-DockerImages.ps1 -Version "dev" -SkipPush + +# Release candidate +.\Release-DockerImages.ps1 -Version "1.2.3-rc1" + +# Production release +.\Release-DockerImages.ps1 -Version "1.2.3" +.\Release-DockerImages.ps1 -Version "latest" +``` + +## Support + +For issues or questions about the release process, please refer to the project documentation or contact the development team. diff --git a/src/Hosts/Release-DockerImages.ps1 b/src/Hosts/Release-DockerImages.ps1 new file mode 100644 index 00000000..be10f3be --- /dev/null +++ b/src/Hosts/Release-DockerImages.ps1 @@ -0,0 +1,423 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Builds and releases Docker images for Budget application (Client and Server) + +.DESCRIPTION + This script builds Docker images for both the client and server components, + optionally tags them with version information, and pushes them to a Docker registry. + +.PARAMETER Version + Version tag for the images (e.g., "1.0.0", "latest"). Default is "latest" + +.PARAMETER SkipBuild + Skip building images and only push existing ones + +.PARAMETER SkipPush + Build images but skip pushing to registry + +.PARAMETER ConfigureRegistry + Force reconfiguration of Docker registry settings + +.EXAMPLE + .\Release-DockerImages.ps1 + Builds and pushes images with version "latest" + +.EXAMPLE + .\Release-DockerImages.ps1 -Version "1.2.3" + Builds and pushes images with version "1.2.3" + +.EXAMPLE + .\Release-DockerImages.ps1 -SkipPush + Builds images locally without pushing to registry +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$Version = "latest", + + [Parameter(Mandatory=$false)] + [switch]$SkipBuild, + + [Parameter(Mandatory=$false)] + [switch]$SkipPush, + + [Parameter(Mandatory=$false)] + [switch]$ConfigureRegistry +) + +# Script configuration +$ErrorActionPreference = "Stop" +$ConfigFile = Join-Path $PSScriptRoot "docker-registry-config.json" + +# Color functions for better output +function Write-ColorOutput { + param( + [string]$Message, + [string]$ForegroundColor = "White" + ) + Write-Host $Message -ForegroundColor $ForegroundColor +} + +function Write-Success { + param([string]$Message) + Write-ColorOutput "✓ $Message" "Green" +} + +function Write-Info { + param([string]$Message) + Write-ColorOutput "ℹ $Message" "Cyan" +} + +function Write-Warning { + param([string]$Message) + Write-ColorOutput "⚠ $Message" "Yellow" +} + +function Write-ErrorMessage { + param([string]$Message) + Write-ColorOutput "✗ $Message" "Red" +} + +function Write-Step { + param([string]$Message) + Write-ColorOutput "`n==> $Message" "Magenta" +} + +# Load or create registry configuration +function Get-RegistryConfig { + if (Test-Path $ConfigFile) { + try { + $config = Get-Content $ConfigFile -Raw | ConvertFrom-Json + return $config + } + catch { + Write-Warning "Failed to read config file. Will create new configuration." + return $null + } + } + return $null +} + +# Save registry configuration +function Save-RegistryConfig { + param($Config) + + try { + $Config | ConvertTo-Json | Set-Content $ConfigFile + Write-Success "Registry configuration saved to: $ConfigFile" + } + catch { + Write-ErrorMessage "Failed to save configuration: $_" + } +} + +# Configure Docker registry +function Initialize-RegistryConfig { + Write-Step "Docker Registry Configuration" + + Write-Info "Please provide Docker registry information:" + Write-Info "(Press Enter to skip and use local images only)" + Write-Host "" + + $registry = Read-Host "Docker Registry URL (e.g., docker.io, ghcr.io, registry.example.com)" + + if ([string]::IsNullOrWhiteSpace($registry)) { + Write-Warning "No registry configured. Images will only be built locally." + return $null + } + + $username = Read-Host "Registry Username" + $securePassword = Read-Host "Registry Password" -AsSecureString + + # Convert SecureString to encrypted string for storage + $encryptedPassword = ConvertFrom-SecureString $securePassword + + $namespace = Read-Host "Image Namespace/Repository (e.g., mycompany/budget)" + + $config = @{ + Registry = $registry + Username = $username + EncryptedPassword = $encryptedPassword + Namespace = $namespace + ConfiguredDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + } + + Save-RegistryConfig -Config $config + return $config +} + +# Docker login +function Connect-DockerRegistry { + param($Config) + + if (-not $Config) { + return $false + } + + try { + Write-Step "Logging into Docker Registry" + + $securePassword = ConvertTo-SecureString $Config.EncryptedPassword + $credential = New-Object System.Management.Automation.PSCredential($Config.Username, $securePassword) + $plainPassword = $credential.GetNetworkCredential().Password + + $plainPassword | docker login $Config.Registry --username $Config.Username --password-stdin + + if ($LASTEXITCODE -ne 0) { + Write-ErrorMessage "Docker login failed" + return $false + } + + Write-Success "Successfully logged into $($Config.Registry)" + return $true + } + catch { + Write-ErrorMessage "Failed to login to Docker registry: $_" + return $false + } +} + +# Build Docker image +function Build-DockerImage { + param( + [string]$Name, + [string]$DockerfilePath, + [string]$Context, + [string]$Tag + ) + + Write-Step "Building $Name" + Write-Info "Dockerfile: $DockerfilePath" + Write-Info "Context: $Context" + Write-Info "Tag: $Tag" + + try { + docker build -f $DockerfilePath -t $Tag $Context + + if ($LASTEXITCODE -ne 0) { + Write-ErrorMessage "Failed to build $Name" + return $false + } + + Write-Success "Successfully built $Name" + return $true + } + catch { + Write-ErrorMessage "Error building $Name : $_" + return $false + } +} + +# Tag and push image +function Publish-DockerImage { + param( + [string]$LocalTag, + [string]$RemoteTag, + [string]$Name + ) + + Write-Step "Publishing $Name" + + try { + # Tag image for registry + Write-Info "Tagging image: $LocalTag -> $RemoteTag" + docker tag $LocalTag $RemoteTag + + if ($LASTEXITCODE -ne 0) { + Write-ErrorMessage "Failed to tag $Name" + return $false + } + + # Push to registry + Write-Info "Pushing image: $RemoteTag" + docker push $RemoteTag + + if ($LASTEXITCODE -ne 0) { + Write-ErrorMessage "Failed to push $Name" + return $false + } + + Write-Success "Successfully pushed $Name to registry" + return $true + } + catch { + Write-ErrorMessage "Error publishing $Name : $_" + return $false + } +} + +# Main script execution +function Main { + Write-ColorOutput @" +╔═══════════════════════════════════════════════════════════╗ +║ ║ +║ Budget Application - Docker Release Script ║ +║ ║ +╚═══════════════════════════════════════════════════════════╝ +"@ "Cyan" + + Write-Info "Version: $Version" + Write-Info "Skip Build: $SkipBuild" + Write-Info "Skip Push: $SkipPush" + Write-Host "" + + # Determine paths + $scriptDir = $PSScriptRoot + $hostsDir = $scriptDir + $srcDir = Split-Path -Parent $hostsDir + $repoRoot = Split-Path -Parent $srcDir + + Write-Info "Repository Root: $repoRoot" + Write-Info "Source Directory: $srcDir" + Write-Host "" + + # Check if Docker is available + try { + docker --version | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "Docker is not available" + } + } + catch { + Write-ErrorMessage "Docker is not installed or not in PATH" + exit 1 + } + + # Load or configure registry + $config = Get-RegistryConfig + + if ($ConfigureRegistry -or (-not $config -and -not $SkipPush)) { + $config = Initialize-RegistryConfig + } + + # Define image names + $serverImageName = "budget-server" + $clientImageName = "budget-client" + + # Local tags + $serverLocalTag = "${serverImageName}:${Version}" + $clientLocalTag = "${clientImageName}:${Version}" + + # Remote tags (if registry is configured) + $serverRemoteTag = $null + $clientRemoteTag = $null + + if ($config -and $config.Registry) { + $registryPrefix = "$($config.Registry)" + if ($config.Namespace) { + $registryPrefix = "$registryPrefix/$($config.Namespace)" + } + $serverRemoteTag = "${registryPrefix}/${serverImageName}:${Version}" + $clientRemoteTag = "${registryPrefix}/${clientImageName}:${Version}" + } + + # Build images + if (-not $SkipBuild) { + Write-Step "Starting Docker Image Build Process" + + # Build Server + $serverDockerfile = Join-Path $hostsDir "NVs.Budget.Hosts.Web.Server\Dockerfile" + $serverSuccess = Build-DockerImage ` + -Name "Server" ` + -DockerfilePath $serverDockerfile ` + -Context $repoRoot ` + -Tag $serverLocalTag + + if (-not $serverSuccess) { + Write-ErrorMessage "Server build failed. Aborting." + exit 1 + } + + # Build Client + $clientDockerfile = Join-Path $hostsDir "NVs.Budget.Hosts.Web.Client\Dockerfile" + $clientContext = Join-Path $hostsDir "NVs.Budget.Hosts.Web.Client" + $clientSuccess = Build-DockerImage ` + -Name "Client" ` + -DockerfilePath $clientDockerfile ` + -Context $clientContext ` + -Tag $clientLocalTag + + if (-not $clientSuccess) { + Write-ErrorMessage "Client build failed. Aborting." + exit 1 + } + + Write-Success "`nAll images built successfully!" + + # Display local images + Write-Step "Local Images Built" + docker images | Select-String -Pattern "(REPOSITORY|$serverImageName|$clientImageName)" + } + else { + Write-Warning "Skipping build phase" + } + + # Push images to registry + if (-not $SkipPush -and $config -and $config.Registry) { + # Login to registry + $loginSuccess = Connect-DockerRegistry -Config $config + + if (-not $loginSuccess) { + Write-Warning "Failed to login to registry. Skipping push." + } + else { + # Push Server + $serverPushSuccess = Publish-DockerImage ` + -LocalTag $serverLocalTag ` + -RemoteTag $serverRemoteTag ` + -Name "Server" + + # Push Client + $clientPushSuccess = Publish-DockerImage ` + -LocalTag $clientLocalTag ` + -RemoteTag $clientRemoteTag ` + -Name "Client" + + if ($serverPushSuccess -and $clientPushSuccess) { + Write-Success "`nAll images published successfully!" + Write-Host "" + Write-Info "Server Image: $serverRemoteTag" + Write-Info "Client Image: $clientRemoteTag" + } + else { + Write-ErrorMessage "`nSome images failed to publish" + exit 1 + } + } + } + elseif (-not $SkipPush -and (-not $config -or -not $config.Registry)) { + Write-Warning "`nNo registry configured. Images are available locally only." + Write-Info "Run with -ConfigureRegistry to set up Docker registry." + } + else { + Write-Warning "`nSkipping push phase" + } + + Write-Host "" + Write-Step "Release Process Complete" + Write-Host "" + Write-Success "Local images are tagged as:" + Write-Host " - $serverLocalTag" + Write-Host " - $clientLocalTag" + + if ($config -and $config.Registry -and -not $SkipPush) { + Write-Host "" + Write-Success "Remote images are available at:" + Write-Host " - $serverRemoteTag" + Write-Host " - $clientRemoteTag" + } + + Write-Host "" +} + +# Run main function +try { + Main +} +catch { + Write-ErrorMessage "An unexpected error occurred: $_" + Write-ErrorMessage $_.ScriptStackTrace + exit 1 +} diff --git a/src/Hosts/docker-registry-config.example.json b/src/Hosts/docker-registry-config.example.json new file mode 100644 index 00000000..fb287d35 --- /dev/null +++ b/src/Hosts/docker-registry-config.example.json @@ -0,0 +1,7 @@ +{ + "Registry": "docker.io", + "Username": "your-username", + "EncryptedPassword": "01000000d08c9ddf0115d1118c7a00c04fc297eb...", + "Namespace": "your-namespace/budget", + "ConfiguredDate": "2026-01-17 12:00:00" +} diff --git a/src/Hosts/web-debug/.gitignore b/src/Hosts/web-debug/.gitignore new file mode 100644 index 00000000..0586ac2d --- /dev/null +++ b/src/Hosts/web-debug/.gitignore @@ -0,0 +1,6 @@ +# Generated certificates +certs/ + +# Environment file with secrets +server.env + diff --git a/src/Hosts/web-debug/README.md b/src/Hosts/web-debug/README.md new file mode 100644 index 00000000..9273089a --- /dev/null +++ b/src/Hosts/web-debug/README.md @@ -0,0 +1,142 @@ +# Budget Application - Development Scripts + +This directory contains scripts to run the Budget application in development mode with live reload/watch capabilities. + +## Architecture + +- **Docker Services**: PostgreSQL database and SSL certificate generation +- **Host Services**: .NET server and Angular client running with watch mode for fast iteration + +## Prerequisites + +### Required Software +- Docker Desktop +- PowerShell 7+ +- .NET SDK 8.0+ +- Node.js 20+ +- npm + +### Configuration +1. Copy `server.env.example` to `server.env` +2. Fill in your Yandex OAuth credentials in `server.env`: + ``` + Auth__Yandex__ClientSecret = your_client_secret + Auth__Yandex__ClientId = your_client_id + ``` + +## Quick Start + +### Start All Services +```powershell +.\start-all.ps1 +``` + +This will: +1. Start PostgreSQL and generate SSL certificates in Docker +2. Extract certificates to the `certs/` directory +3. Launch the .NET server with watch mode in a new window +4. Launch the Angular client with watch mode in a new window + +### Start Services Individually + +**Start only Docker dependencies:** +```powershell +docker compose up -d +``` + +**Start only the server:** +```powershell +.\start-server.ps1 +``` + +**Start only the client:** +```powershell +.\start-client.ps1 +``` + +### Stop All Services +```powershell +.\stop-all.ps1 +``` + +Then manually stop server/client processes (Ctrl+C in their windows). + +## Service URLs + +- **Server (HTTPS)**: https://localhost:7237 +- **Server (HTTP)**: http://localhost:5153 +- **Client**: http://localhost:4200 +- **PostgreSQL**: localhost:20000 + +## Development Workflow + +1. Start all services with `.\start-all.ps1` +2. Make changes to your code +3. Watch mode will automatically detect changes and reload: + - **.NET Server**: `dotnet watch` rebuilds and restarts + - **Angular Client**: Hot module replacement (HMR) +4. View changes in your browser + +## Troubleshooting + +### Certificate Issues +If you encounter SSL certificate errors: +```powershell +# Delete the certs directory +Remove-Item -Recurse -Force .\certs + +# Restart services to regenerate +.\start-all.ps1 +``` + +### Database Connection Issues +```powershell +# Check if PostgreSQL is running +docker compose ps + +# View PostgreSQL logs +docker compose logs postgres + +# Restart PostgreSQL +docker compose restart postgres +``` + +### Port Already in Use +If ports are already in use, stop any conflicting services: +- Server: 7237 (HTTPS), 5153 (HTTP) +- Client: 4200 +- PostgreSQL: 20000 + +## Docker Volumes + +- `web-debug_budgetdb-data`: PostgreSQL data (persistent) +- `web-debug_certs`: SSL certificates + +To reset the database: +```powershell +docker compose down -v +``` + +## Files Structure + +``` +web-debug/ +├── docker-compose.yml # Docker services (postgres, dev-certs) +├── dev-certs.Dockerfile # Certificate generation +├── server.env # Server environment variables (gitignored) +├── server.env.example # Template for server.env +├── start-all.ps1 # Master script to start everything +├── start-server.ps1 # Start .NET server on host +├── start-client.ps1 # Start Angular client on host +├── stop-all.ps1 # Stop Docker services +└── README.md # This file +``` + +## Benefits of This Approach + +✅ **Fast Iteration**: Watch mode catches changes instantly +✅ **Better Debugging**: Direct access to processes on host +✅ **Isolated Dependencies**: Database runs in Docker +✅ **Flexible Development**: Start/stop services independently +✅ **Production-like**: SSL certificates and proper configuration + diff --git a/src/Hosts/web-debug/dev-certs.Dockerfile b/src/Hosts/web-debug/dev-certs.Dockerfile new file mode 100644 index 00000000..14561d36 --- /dev/null +++ b/src/Hosts/web-debug/dev-certs.Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG APP_UID=1000 +WORKDIR /https +RUN chmod 755 /https +RUN dotnet dev-certs https -ep /https/aspnetapp.pfx -p "dev-password-do-not-use-in-production" + +# Export certificate (CRT) from PFX file +RUN openssl pkcs12 -in /https/aspnetapp.pfx -out /https/aspnetapp.crt -nokeys -passin pass:"dev-password-do-not-use-in-production" + +# Export private key (KEY) from PFX file +RUN openssl pkcs12 -in /https/aspnetapp.pfx -out /https/aspnetapp.key -nocerts -nodes -passin pass:"dev-password-do-not-use-in-production" + +RUN chmod 644 /https/* +RUN chown $APP_UID:$APP_UID /https/* \ No newline at end of file diff --git a/src/Hosts/docker-compose.yml b/src/Hosts/web-debug/docker-compose.yml similarity index 62% rename from src/Hosts/docker-compose.yml rename to src/Hosts/web-debug/docker-compose.yml index 62325bcb..84623b10 100644 --- a/src/Hosts/docker-compose.yml +++ b/src/Hosts/web-debug/docker-compose.yml @@ -1,7 +1,6 @@ -version: "3.9" services: postgres: - image: postgres + image: postgres:17 environment: POSTGRES_DB: "budgetdb" POSTGRES_USER: "postgres" @@ -12,5 +11,15 @@ services: ports: - "20000:5432" + dev-certs: + build: + context: . + dockerfile: ./dev-certs.Dockerfile + args: + - APP_UID=1000 + volumes: + - certs:/https + volumes: budgetdb-data: + certs: diff --git a/src/Hosts/web-debug/server.env.example b/src/Hosts/web-debug/server.env.example new file mode 100644 index 00000000..bda5674f --- /dev/null +++ b/src/Hosts/web-debug/server.env.example @@ -0,0 +1,2 @@ +Auth__Yandex__ClientSecret = your_client_secret +Auth__Yandex__ClientId = your_client_id diff --git a/src/Hosts/web-debug/start-all.ps1 b/src/Hosts/web-debug/start-all.ps1 new file mode 100644 index 00000000..7f5d95dc --- /dev/null +++ b/src/Hosts/web-debug/start-all.ps1 @@ -0,0 +1,111 @@ +#!/usr/bin/env pwsh +# Master script to start all Budget application services +# This script starts Docker dependencies and runs server/client on the host + +# Set error action preference +$ErrorActionPreference = "Stop" + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Budget Application - Development Mode" -ForegroundColor Cyan +Write-Host "========================================`n" -ForegroundColor Cyan + +# Navigate to script directory +Set-Location $PSScriptRoot + +# Step 1: Start Docker services (postgres and dev-certs) +Write-Host "[1/4] Starting Docker services (postgres, dev-certs)..." -ForegroundColor Yellow +docker compose up -d postgres dev-certs +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to start Docker services!" -ForegroundColor Red + exit 1 +} +Write-Host "Docker services started successfully!`n" -ForegroundColor Green + +# Step 2: Wait for postgres to be ready +Write-Host "[2/4] Waiting for PostgreSQL to be ready..." -ForegroundColor Yellow +$maxAttempts = 30 +$attempt = 0 +$ready = $false + +while (-not $ready -and $attempt -lt $maxAttempts) { + $attempt++ + try { + $result = docker exec (docker compose ps -q postgres) pg_isready -U postgres 2>&1 + if ($result -match "accepting connections") { + $ready = $true + } + } + catch { + # Ignore errors during connection attempts + } + + if (-not $ready) { + Write-Host " Attempt $attempt/$maxAttempts - Waiting..." -ForegroundColor Gray + Start-Sleep -Seconds 1 + } +} + +if (-not $ready) { + Write-Host "PostgreSQL failed to start within timeout!" -ForegroundColor Red + exit 1 +} +Write-Host "PostgreSQL is ready!`n" -ForegroundColor Green + +# Step 3: Extract certificates if needed +Write-Host "[3/4] Setting up SSL certificates..." -ForegroundColor Yellow +$certsPath = Join-Path $PSScriptRoot "certs" +if (-not (Test-Path $certsPath)) { + New-Item -ItemType Directory -Path $certsPath -Force | Out-Null +} + +$certFile = Join-Path $certsPath "aspnetapp.pfx" +if (-not (Test-Path $certFile)) { + Write-Host " Extracting certificates from Docker volume..." -ForegroundColor Gray + + # Wait for dev-certs to complete + Start-Sleep -Seconds 2 + + # Copy certs from Docker volume + $containerId = docker create -v "web-debug_certs:/https" alpine + docker cp "${containerId}:/https/." $certsPath | Out-Null + docker rm $containerId | Out-Null + + Write-Host " Certificates extracted successfully!" -ForegroundColor Green +} else { + Write-Host " Certificates already exist." -ForegroundColor Green +} +Write-Host "" + +# Step 4: Start server and client +Write-Host "[4/4] Starting server and client..." -ForegroundColor Yellow +Write-Host @" + +================================================================================ +SERVICES STARTING +================================================================================ + +The following services will start in separate windows: + - .NET Server (https://localhost:7237, http://localhost:5153) + - Angular Client (http://localhost:4200) + +Docker Services Running: + - PostgreSQL (localhost:20000) + +Press Ctrl+C in any window to stop that service. +To stop all services, close all windows and run: docker compose down + +================================================================================ + +"@ -ForegroundColor Cyan + +# Start server in a new PowerShell window +$serverScript = Join-Path $PSScriptRoot "start-server.ps1" + wt sp -d . pwsh $serverScript + +# Wait a moment before starting client +Start-Sleep -Seconds 2 + +# Start client in a new PowerShell window +$clientScript = Join-Path $PSScriptRoot "start-client.ps1" +pwsh $clientScript + diff --git a/src/Hosts/web-debug/start-client.ps1 b/src/Hosts/web-debug/start-client.ps1 new file mode 100644 index 00000000..718f9f11 --- /dev/null +++ b/src/Hosts/web-debug/start-client.ps1 @@ -0,0 +1,32 @@ +#!/usr/bin/env pwsh +# Start the Angular Budget Client with watch mode on the host machine +# Prerequisites: Node.js and npm must be installed + +# Set error action preference +$ErrorActionPreference = "Stop" + +Write-Host "Starting Budget Client..." -ForegroundColor Green + +# Navigate to client directory +$clientPath = Join-Path $PSScriptRoot "..\NVs.Budget.Hosts.Web.Client\budget-client" +Set-Location $clientPath + +# Check if node_modules exists +if (-not (Test-Path "node_modules")) { + Write-Host "node_modules not found. Installing dependencies..." -ForegroundColor Yellow + npm install + Write-Host "Dependencies installed successfully!" -ForegroundColor Green +} + +# Set environment +$env:NODE_ENV = "development" + +Write-Host "`nEnvironment Configuration:" -ForegroundColor Cyan +Write-Host " Development Server: http://localhost:4200" -ForegroundColor White +Write-Host " Watch Mode: Enabled" -ForegroundColor White +Write-Host "`nStarting Angular dev server..." -ForegroundColor Green +Write-Host "----------------------------------------`n" -ForegroundColor Gray + +# Start Angular dev server with watch +npm start + diff --git a/src/Hosts/web-debug/start-server.ps1 b/src/Hosts/web-debug/start-server.ps1 new file mode 100644 index 00000000..4b3252d9 --- /dev/null +++ b/src/Hosts/web-debug/start-server.ps1 @@ -0,0 +1,76 @@ +#!/usr/bin/env pwsh +# Start the .NET Budget Server with watch mode on the host machine +# Prerequisites: Docker Compose must be running (postgres and dev-certs services) + +param( + [string]$CertsPath = ".\certs" +) + +# Set error action preference +$ErrorActionPreference = "Stop" + +Write-Host "Starting Budget Server..." -ForegroundColor Green + +# Create certs directory if it doesn't exist +if (-not (Test-Path $CertsPath)) { + New-Item -ItemType Directory -Path $CertsPath -Force | Out-Null +} + +# Get absolute path for certs +$CertsAbsPath = (Resolve-Path $CertsPath).Path + +# Check if certs exist, if not extract from Docker volume +$certFile = Join-Path $CertsAbsPath "aspnetapp.pfx" +if (-not (Test-Path $certFile)) { + Write-Host "Extracting certificates from Docker volume..." -ForegroundColor Yellow + + # Ensure dev-certs container has run + docker compose up dev-certs --build + + # Extract certs from Docker volume + docker compose run --rm dev-certs sh -c "cp /https/* /tmp/" 2>&1 | Out-Null + + # Use a temporary container to copy files from the volume + $containerId = docker create -v "web-debug_certs:/https" alpine + docker cp "${containerId}:/https/." $CertsAbsPath + docker rm $containerId | Out-Null + + Write-Host "Certificates extracted to $CertsAbsPath" -ForegroundColor Green +} + +# Load Yandex Auth credentials from server.env +$serverEnvPath = Join-Path $PSScriptRoot "server.env" +if (Test-Path $serverEnvPath) { + Get-Content $serverEnvPath | ForEach-Object { + if ($_ -match '^\s*([^#][^=]*?)\s*=\s*(.+?)\s*$') { + $key = $matches[1].Trim() + $value = $matches[2].Trim() + [Environment]::SetEnvironmentVariable($key, $value, "Process") + Write-Host "Loaded: $key" -ForegroundColor Cyan + } + } +} else { + Write-Host "Warning: server.env not found at $serverEnvPath" -ForegroundColor Yellow +} + +# Set environment variables +$env:ASPNETCORE_ENVIRONMENT = "Development" +$env:ConnectionStrings__BudgetContext = "Host=localhost;Port=20000;Database=budgetdb;Username=postgres;Password=postgres" +$env:ConnectionStrings__IdentityContext = "Host=localhost;Port=20000;Database=budgetdb;Username=postgres;Password=postgres" +$env:ASPNETCORE_Kestrel__Certificates__Default__Path = $certFile +$env:ASPNETCORE_Kestrel__Certificates__Default__Password = "dev-password-do-not-use-in-production" + +# Navigate to server directory +$serverPath = Join-Path $PSScriptRoot "..\NVs.Budget.Hosts.Web.Server" +Set-Location $serverPath + +Write-Host "`nEnvironment Configuration:" -ForegroundColor Cyan +Write-Host " Database: localhost:20000/budgetdb" -ForegroundColor White +Write-Host " HTTPS Certificate: $certFile" -ForegroundColor White +Write-Host " Launch Profile: https (7237, 5153)" -ForegroundColor White +Write-Host "`nStarting dotnet watch..." -ForegroundColor Green +Write-Host "----------------------------------------`n" -ForegroundColor Gray + +# Run dotnet watch +dotnet watch run --project NVs.Budget.Hosts.Web.Server.csproj --launch-profile https + diff --git a/src/Hosts/web-debug/stop-all.ps1 b/src/Hosts/web-debug/stop-all.ps1 new file mode 100644 index 00000000..7fa664a9 --- /dev/null +++ b/src/Hosts/web-debug/stop-all.ps1 @@ -0,0 +1,15 @@ +#!/usr/bin/env pwsh +# Stop all Budget application services + +Write-Host "Stopping all Budget application services..." -ForegroundColor Yellow + +# Navigate to script directory +Set-Location $PSScriptRoot + +# Stop Docker services +Write-Host "Stopping Docker containers..." -ForegroundColor Cyan +docker compose down + +Write-Host "`nDocker services stopped!" -ForegroundColor Green +Write-Host "Note: Server and client processes in other windows need to be stopped manually (Ctrl+C)." -ForegroundColor Yellow + diff --git a/src/Hosts/web-release/.env.example b/src/Hosts/web-release/.env.example new file mode 100644 index 00000000..a4ff5568 --- /dev/null +++ b/src/Hosts/web-release/.env.example @@ -0,0 +1,29 @@ +# Database Configuration +POSTGRES_DB=budgetdb +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your_secure_password_here + +# Backup Configuration +BACKUP_PATH=/path/to/your/backups +BACKUP_SCHEDULE=@daily +BACKUP_KEEP_DAYS=7 +BACKUP_KEEP_WEEKS=4 +BACKUP_KEEP_MONTHS=6 + +# SSL Certificate Configuration +CERT_HOSTNAME=budget.yourdomain.com +CERT_DAYS=365 +CERTGEN_VERSION=latest + +# Docker Registry Configuration +DOCKER_REGISTRY=docker.io +DOCKER_NAMESPACE=yournamespace +IMAGE_VERSION=latest + +# Service Ports +CLIENT_PORT=25000 +SERVER_PORT=25001 + +# Yandex OAuth Configuration +YANDEX_OAUTH_CLIENT_ID=your_yandex_client_id +YANDEX_OAUTH_CLIENT_SECRET=your_yandex_client_secret diff --git a/src/Hosts/web-release/.gitignore b/src/Hosts/web-release/.gitignore new file mode 100644 index 00000000..dbbca9eb --- /dev/null +++ b/src/Hosts/web-release/.gitignore @@ -0,0 +1,14 @@ +# Environment configuration +.env + +# Docker volumes +data/ + +# Backup files +*.sql +*.sql.gz + +# Certificates +*.pem +*.crt +*.key diff --git a/src/Hosts/web-release/README.md b/src/Hosts/web-release/README.md new file mode 100644 index 00000000..39a9fb77 --- /dev/null +++ b/src/Hosts/web-release/README.md @@ -0,0 +1,399 @@ +# Budget Application - Production Release + +This directory contains the production-ready Docker Compose configuration for the Budget application with HTTPS support, automatic database backups, and proper service orchestration. + +## Services Overview + +- **budget-client** - Angular client application (served by Kestrel with HTTPS) +- **budget-server** - ASP.NET Core API server (Kestrel with HTTPS) +- **postgres** - PostgreSQL 17 database +- **pgbackups** - Automated database backup service +- **cert-generator** - Self-signed SSL certificate generator (uses [nvs-certgen](https://github.com/nvsnkv/certgen)) + +## Prerequisites + +- Docker and Docker Compose installed +- Access to Docker registry with the budget images +- Yandex OAuth credentials (for authentication) + +## Quick Start + +### 1. Setup Environment Variables + +Copy the example environment file and configure it: + +```bash +cp .env.example .env +``` + +Edit `.env` and configure the following **required** variables: + +```env +# Database password (change this!) +POSTGRES_PASSWORD=your_secure_password_here + +# Backup location (absolute path) +BACKUP_PATH=/path/to/your/backups + +# SSL certificate hostname +CERT_HOSTNAME=budget.yourdomain.com + +# Docker registry details +DOCKER_REGISTRY=docker.io +DOCKER_NAMESPACE=yournamespace +IMAGE_VERSION=latest + +# Yandex OAuth credentials +YANDEX_OAUTH_CLIENT_ID=your_yandex_client_id +YANDEX_OAUTH_CLIENT_SECRET=your_yandex_client_secret +``` + +### 2. Pull or Build Images + +If you need to build the images first: + +```bash +# Navigate to the Hosts directory +cd ../ + +# Build and push images using the release script +.\Release-DockerImages.ps1 -Version "1.0.0" + +# Or just build locally +.\Release-DockerImages.ps1 -Version "1.0.0" -SkipPush +``` + +If images are already in your registry, they will be pulled automatically. + +### 3. Start the Application + +```bash +docker-compose up -d +``` + +This will: +1. Create the network and volumes +2. Start PostgreSQL database +3. Generate self-signed SSL certificates +4. Start the budget-client (Kestrel with HTTPS) +5. Start the budget-server (Kestrel with HTTPS) +6. Start the backup service + +### 4. Access the Application + +- **Client (Frontend)**: `https://localhost:25000` (or your configured `https://CERT_HOSTNAME:CLIENT_PORT`) +- **Server (API)**: `https://localhost:25001` (or your configured `https://CERT_HOSTNAME:SERVER_PORT`) + +**Note**: Since we're using self-signed certificates, you'll need to accept the security warning in your browser. + +Both services use HTTPS directly with Kestrel and share the same SSL certificates. + +## Configuration Details + +### Port Configuration + +The application exposes two HTTPS ports (configurable via `.env`): + +- `CLIENT_PORT` (default: 25000) - Client application HTTPS port +- `SERVER_PORT` (default: 25001) - Server API HTTPS port + +**Note**: The frontend URL is automatically constructed as `https://${CERT_HOSTNAME}:${CLIENT_PORT}` and passed to the budget-server for CORS and OAuth redirects. + +### Architecture + +Both the client and server use Kestrel with HTTPS enabled: + +- **Client**: Kestrel serves the Angular application over HTTPS on `CLIENT_PORT` +- **Server**: Kestrel serves the API over HTTPS on `SERVER_PORT` +- **SSL Certificates**: Both services share the same SSL certificates generated by cert-generator +- **Runtime Configuration**: The Angular app loads its API URL dynamically from `/api/config` endpoint, allowing the same client image to work in different environments + +This provides a uniform architecture where both services are ASP.NET Core applications using the same web server (Kestrel) with identical HTTPS configuration. + +#### Runtime Configuration + +The client application uses runtime configuration instead of build-time configuration: + +1. On startup, the Angular app calls `/api/config` endpoint +2. The ASP.NET Core server returns the API URL from environment variables +3. The `APP_INITIALIZER` sets the API URL before the app starts +4. This allows the same Docker image to be deployed in different environments without rebuilding + +### Database Backups + +The `pgbackups` service automatically backs up the PostgreSQL database: + +- **Schedule**: Daily by default (configurable via `BACKUP_SCHEDULE`) +- **Retention**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 6 months +- **Location**: Specified by `BACKUP_PATH` in `.env` + +**Important**: Ensure the backup path exists and has proper permissions: + +```bash +# Linux/Mac +mkdir -p /path/to/your/backups +chmod 755 /path/to/your/backups + +# Windows +New-Item -Path "C:\backups\budget" -ItemType Directory -Force +``` + +### SSL Certificates + +The application uses [nvs-certgen](https://github.com/nvsnkv/certgen) to automatically generate self-signed SSL certificates on first run. The certificate is valid for 365 days by default (configurable via `CERT_DAYS`). + +Certificate files: +- `tls.crt` - Certificate file +- `tls.key` - Private key + +For production use with real domain names, you should: + +1. **Option A**: Use Let's Encrypt with Certbot +2. **Option B**: Replace the `cert-generator` service with your own certificates: + +```yaml +# Mount your own certificates (must be named tls.crt and tls.key) +budget-client: + volumes: + - ./my-certs:/certs:ro + +budget-server: + volumes: + - ./my-certs:/certs:ro +``` + +**Note**: If you need to regenerate certificates (e.g., after hostname change or expiration), remove the certs volume: + +```bash +docker-compose down +docker volume rm web-release_certs +docker-compose up -d +``` + +## Management Commands + +### View Logs + +```bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f budget-server +docker-compose logs -f budget-client +docker-compose logs -f pgbackups +``` + +### Stop Application + +```bash +docker-compose down +``` + +### Stop and Remove Data + +```bash +# Warning: This will delete all data including database! +docker-compose down -v +``` + +### Restart a Service + +```bash +docker-compose restart budget-server +``` + +### Update to New Version + +```bash +# Update .env with new version +# IMAGE_VERSION=1.0.1 + +# Pull new images and restart +docker-compose pull +docker-compose up -d +``` + +### Check Service Health + +```bash +docker-compose ps +``` + +## Backup and Restore + +### Manual Backup + +```bash +docker-compose exec postgres pg_dump -U postgres budgetdb > backup.sql +``` + +### Restore from Backup + +```bash +# Stop the application +docker-compose down + +# Remove old database volume +docker volume rm web-release_postgres-data + +# Start only postgres +docker-compose up -d postgres + +# Wait for postgres to be ready (check logs) +docker-compose logs -f postgres + +# Restore backup +cat backup.sql | docker-compose exec -T postgres psql -U postgres budgetdb + +# Start all services +docker-compose up -d +``` + +### Access Automated Backups + +Backups are stored in the directory specified by `BACKUP_PATH`: + +```bash +# List backups +ls -lah /path/to/your/backups + +# Backups are named: daily/budgetdb-YYYYMMDD-HHMMSS.sql.gz +``` + +## Troubleshooting + +### Cannot Connect to Application + +1. Check all services are running: + ```bash + docker-compose ps + ``` + +2. Check client logs: + ```bash + docker-compose logs budget-client + ``` + +3. Check server logs: + ```bash + docker-compose logs budget-server + ``` + +4. Verify certificates were generated: + ```bash + docker-compose logs cert-generator + ``` + +### Database Connection Issues + +1. Check postgres is healthy: + ```bash + docker-compose ps postgres + ``` + +2. Check server logs: + ```bash + docker-compose logs budget-server + ``` + +3. Verify database credentials in `.env` + +### OAuth Authentication Issues + +1. Verify Yandex OAuth credentials in `.env` +2. Check that the redirect URI is configured in Yandex OAuth settings +3. Check server logs for authentication errors + +### Backup Issues + +1. Verify `BACKUP_PATH` exists and is accessible: + ```bash + # Linux/Mac + ls -ld /path/to/your/backups + + # Windows + Test-Path "C:\backups\budget" + ``` + +2. Check backup service logs: + ```bash + docker-compose logs pgbackups + ``` + +### Certificate Issues + +If you encounter certificate issues or need to regenerate certificates: + +1. Check cert-generator logs: + ```bash + docker-compose logs cert-generator + ``` + +2. Regenerate certificates: + ```bash + # Remove the certs volume + docker-compose down + docker volume rm web-release_certs + + # Restart (will regenerate certificates) + docker-compose up -d + ``` + +3. Verify hostname configuration in `.env` matches your domain + +## Production Recommendations + +1. **Use Real SSL Certificates**: Replace self-signed certificates with proper SSL certificates from Let's Encrypt or a certificate authority + +2. **Secure Database Password**: Use a strong, randomly generated password for PostgreSQL + +3. **Regular Backups**: Monitor the backup service and test restores regularly + +4. **Resource Limits**: Add resource limits to services in docker-compose.yml: + ```yaml + deploy: + resources: + limits: + cpus: '1' + memory: 1G + ``` + +5. **Monitoring**: Consider adding monitoring services (Prometheus, Grafana) + +6. **Reverse Proxy**: If running on a server, consider using a proper reverse proxy like Traefik or Caddy for automatic HTTPS with Let's Encrypt + +7. **Firewall**: Ensure only necessary ports are exposed to the internet + +8. **Updates**: Regularly update Docker images and apply security patches + +## Environment Variables Reference + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `POSTGRES_DB` | Database name | budgetdb | No | +| `POSTGRES_USER` | Database user | postgres | No | +| `POSTGRES_PASSWORD` | Database password | - | **Yes** | +| `BACKUP_PATH` | Path for database backups | - | **Yes** | +| `BACKUP_SCHEDULE` | Cron schedule for backups | @daily | No | +| `BACKUP_KEEP_DAYS` | Days to keep daily backups | 7 | No | +| `BACKUP_KEEP_WEEKS` | Weeks to keep weekly backups | 4 | No | +| `BACKUP_KEEP_MONTHS` | Months to keep monthly backups | 6 | No | +| `CERT_HOSTNAME` | SSL certificate hostname | localhost | No | +| `CERT_DAYS` | Certificate validity in days | 365 | No | +| `CERTGEN_VERSION` | nvs-certgen image version | latest | No | +| `DOCKER_REGISTRY` | Docker registry URL (used for all images) | docker.io | **Yes** | +| `DOCKER_NAMESPACE` | Docker namespace/username | - | **Yes** | +| `IMAGE_VERSION` | Image version tag | latest | No | +| `CLIENT_PORT` | Client HTTPS port | 25000 | No | +| `SERVER_PORT` | Server HTTPS port | 25001 | No | +| `ApiUrl` | API URL passed to client (auto-constructed) | https://${CERT_HOSTNAME}:${SERVER_PORT} | No | +| `YANDEX_OAUTH_CLIENT_ID` | Yandex OAuth client ID | - | **Yes** | +| `YANDEX_OAUTH_CLIENT_SECRET` | Yandex OAuth client secret | - | **Yes** | + +## Support + +For issues or questions, please refer to the main project documentation or contact the development team. diff --git a/src/Hosts/web-release/docker-compose.yml b/src/Hosts/web-release/docker-compose.yml new file mode 100644 index 00000000..30baef6b --- /dev/null +++ b/src/Hosts/web-release/docker-compose.yml @@ -0,0 +1,128 @@ +services: + postgres: + image: postgres:17 + container_name: budget-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-budgetdb} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - budget-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] + interval: 10s + timeout: 5s + retries: 5 + + pgbackups: + image: prodrigestivill/postgres-backup-local:17 + container_name: budget-pgbackups + restart: unless-stopped + environment: + POSTGRES_HOST: postgres + POSTGRES_DB: ${POSTGRES_DB:-budgetdb} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + SCHEDULE: ${BACKUP_SCHEDULE:-@daily} + BACKUP_KEEP_DAYS: ${BACKUP_KEEP_DAYS:-7} + BACKUP_KEEP_WEEKS: ${BACKUP_KEEP_WEEKS:-4} + BACKUP_KEEP_MONTHS: ${BACKUP_KEEP_MONTHS:-6} + HEALTHCHECK_PORT: 8080 + volumes: + - ${BACKUP_PATH}:/backups + networks: + - budget-network + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f -k https://localhost:8080 || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + cert-generator: + image: ${DOCKER_REGISTRY}/nvsnkv/certgen:${CERTGEN_VERSION:-latest} + container_name: budget-cert-generator + environment: + CERT_HOSTNAME: ${CERT_HOSTNAME:-localhost} + CERT_DIR: /certs + CERT_FILE: tls.crt + KEY_FILE: tls.key + CERT_DAYS: ${CERT_DAYS:-365} + volumes: + - certs:/certs + networks: + - budget-network + + budget-server: + image: ${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/budget-server:${IMAGE_VERSION:-latest} + container_name: budget-server + restart: unless-stopped + ports: + - "${SERVER_PORT:-25001}:8443" + environment: + ASPNETCORE_ENVIRONMENT: Production + ASPNETCORE_URLS: https://+:8443 + ASPNETCORE_Kestrel__Certificates__Default__Path: /certs/tls.crt + ASPNETCORE_Kestrel__Certificates__Default__KeyPath: /certs/tls.key + ConnectionStrings__BudgetContext: Host=postgres;Database=${POSTGRES_DB:-budgetdb};Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD} + ConnectionStrings__IdentityContext: Host=postgres;Database=${POSTGRES_DB:-budgetdb};Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD} + Auth__Yandex__ClientId: ${YANDEX_OAUTH_CLIENT_ID} + Auth__Yandex__ClientSecret: ${YANDEX_OAUTH_CLIENT_SECRET} + FrontendUrl: https://${CERT_HOSTNAME:-localhost}:${CLIENT_PORT:-25000} + AllowedOrigins: https://${CERT_HOSTNAME:-localhost}:${CLIENT_PORT:-25000} + volumes: + - certs:/certs:ro + networks: + - budget-network + depends_on: + cert-generator: + condition: service_completed_successfully + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f -k https://localhost:8443/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + budget-client: + image: ${DOCKER_REGISTRY}/${DOCKER_NAMESPACE}/budget-client:${IMAGE_VERSION:-latest} + container_name: budget-client + restart: unless-stopped + ports: + - "${CLIENT_PORT:-25000}:8443" + environment: + ASPNETCORE_ENVIRONMENT: Production + ASPNETCORE_URLS: https://+:8443 + ASPNETCORE_Kestrel__Certificates__Default__Path: /certs/tls.crt + ASPNETCORE_Kestrel__Certificates__Default__KeyPath: /certs/tls.key + ApiUrl: https://${CERT_HOSTNAME:-localhost}:${SERVER_PORT:-25001} + volumes: + - certs:/certs:ro + networks: + - budget-network + depends_on: + cert-generator: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "curl -f -k https://localhost:8443/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + budget-network: + driver: bridge + +volumes: + postgres-data: + certs: diff --git a/src/Hosts/web-release/logs.ps1 b/src/Hosts/web-release/logs.ps1 new file mode 100644 index 00000000..fe8b4e77 --- /dev/null +++ b/src/Hosts/web-release/logs.ps1 @@ -0,0 +1,43 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + View logs from Budget application services +.DESCRIPTION + This script displays logs from all or specific services +.PARAMETER Service + Specific service to view logs for (budget-server, budget-client, nginx, postgres, pgbackups) +.PARAMETER Follow + Follow log output +.EXAMPLE + .\logs.ps1 + .\logs.ps1 -Service budget-server + .\logs.ps1 -Service nginx -Follow +#> + +param( + [string]$Service, + [switch]$Follow +) + +$ErrorActionPreference = "Stop" + +$follow_flag = if ($Follow) { "-f" } else { "" } + +if ($Service) { + Write-Host "Viewing logs for: $Service" -ForegroundColor Cyan + if ($follow_flag) { + docker-compose logs $follow_flag $Service + } + else { + docker-compose logs $Service + } +} +else { + Write-Host "Viewing logs for all services" -ForegroundColor Cyan + if ($follow_flag) { + docker-compose logs $follow_flag + } + else { + docker-compose logs + } +} diff --git a/src/Hosts/web-release/start.ps1 b/src/Hosts/web-release/start.ps1 new file mode 100644 index 00000000..9e7e0ab1 --- /dev/null +++ b/src/Hosts/web-release/start.ps1 @@ -0,0 +1,178 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Start the Budget application in production mode +.DESCRIPTION + This script starts all services using docker-compose +.PARAMETER Pull + Pull latest images before starting +.EXAMPLE + .\start.ps1 + .\start.ps1 -Pull +#> + +param( + [switch]$Pull +) + +$ErrorActionPreference = "Stop" + +Write-Host "Budget Application - Production Start" -ForegroundColor Cyan +Write-Host "======================================" -ForegroundColor Cyan +Write-Host "" + +# Check if .env exists +if (-not (Test-Path ".env")) { + Write-Host "❌ .env file not found!" -ForegroundColor Red + Write-Host "" + Write-Host "Please create .env file from .env.example:" -ForegroundColor Yellow + Write-Host " cp .env.example .env" -ForegroundColor Yellow + Write-Host "" + Write-Host "Then edit .env and configure required variables:" -ForegroundColor Yellow + Write-Host " - POSTGRES_PASSWORD" -ForegroundColor Yellow + Write-Host " - BACKUP_PATH" -ForegroundColor Yellow + Write-Host " - DOCKER_REGISTRY" -ForegroundColor Yellow + Write-Host " - DOCKER_NAMESPACE" -ForegroundColor Yellow + Write-Host " - YANDEX_OAUTH_CLIENT_ID" -ForegroundColor Yellow + Write-Host " - YANDEX_OAUTH_CLIENT_SECRET" -ForegroundColor Yellow + exit 1 +} + +# Validate required environment variables +Write-Host "✓ Validating environment configuration..." -ForegroundColor Green +$env_content = Get-Content ".env" +$required_vars = @( + "POSTGRES_PASSWORD", + "BACKUP_PATH", + "DOCKER_REGISTRY", + "DOCKER_NAMESPACE", + "YANDEX_OAUTH_CLIENT_ID", + "YANDEX_OAUTH_CLIENT_SECRET" +) + +$missing_vars = @() +foreach ($var in $required_vars) { + $found = $env_content | Where-Object { $_ -match "^\s*$var\s*=" -and $_ -notmatch "^\s*#" } + if (-not $found) { + $missing_vars += $var + } +} + +if ($missing_vars.Count -gt 0) { + Write-Host "❌ Missing required environment variables:" -ForegroundColor Red + foreach ($var in $missing_vars) { + Write-Host " - $var" -ForegroundColor Red + } + Write-Host "" + Write-Host "Please update your .env file with these values." -ForegroundColor Yellow + exit 1 +} + +# Validate backup path exists +$backup_path_line = $env_content | Where-Object { $_ -match "^\s*BACKUP_PATH\s*=" -and $_ -notmatch "^\s*#" } | Select-Object -First 1 +if ($backup_path_line) { + $backup_path = ($backup_path_line -split "=", 2)[1].Trim() + if ($backup_path -and $backup_path -ne "/path/to/your/backups") { + if (-not (Test-Path $backup_path)) { + Write-Host "⚠️ Backup path does not exist: $backup_path" -ForegroundColor Yellow + $create = Read-Host "Create it now? (y/n)" + if ($create -eq "y") { + New-Item -ItemType Directory -Path $backup_path -Force | Out-Null + Write-Host "✓ Backup directory created" -ForegroundColor Green + } + else { + Write-Host "❌ Cannot continue without backup directory" -ForegroundColor Red + exit 1 + } + } + } +} + +Write-Host "✓ Environment configuration valid" -ForegroundColor Green +Write-Host "" + +# Pull images if requested +if ($Pull) { + Write-Host "Pulling latest images..." -ForegroundColor Cyan + docker-compose pull + if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to pull images" -ForegroundColor Red + exit 1 + } + Write-Host "✓ Images pulled successfully" -ForegroundColor Green + Write-Host "" +} + +# Start services +Write-Host "Starting services..." -ForegroundColor Cyan +docker-compose up -d + +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to start services" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "✓ Services started successfully!" -ForegroundColor Green +Write-Host "" +Write-Host "Waiting for services to be healthy..." + +# Wait for services to be healthy +$max_wait = 60 +$waited = 0 +$all_healthy = $false + +while ($waited -lt $max_wait -and -not $all_healthy) { + Start-Sleep -Seconds 2 + $waited += 2 + + $health = docker-compose ps --format json | ConvertFrom-Json + $unhealthy = $health | Where-Object { $_.Health -ne "healthy" -and $_.Service -ne "cert-generator" } + + if ($unhealthy.Count -eq 0) { + $all_healthy = $true + } + else { + Write-Host "." -NoNewline + } +} + +Write-Host "" +Write-Host "" + +if ($all_healthy) { + Write-Host "✓ All services are healthy!" -ForegroundColor Green +} +else { + Write-Host "⚠️ Some services may still be starting up" -ForegroundColor Yellow + Write-Host " Run 'docker-compose ps' to check status" -ForegroundColor Yellow +} + +Write-Host "" +Write-Host "Application is available at:" -ForegroundColor Cyan + +# Get ports from .env or use defaults +$client_port = ($env_content | Where-Object { $_ -match "^\s*CLIENT_PORT\s*=" } | Select-Object -First 1) +if ($client_port) { + $client_port = ($client_port -split "=", 2)[1].Trim() +} +else { + $client_port = "25000" +} + +$server_port = ($env_content | Where-Object { $_ -match "^\s*SERVER_PORT\s*=" } | Select-Object -First 1) +if ($server_port) { + $server_port = ($server_port -split "=", 2)[1].Trim() +} +else { + $server_port = "25001" +} + +Write-Host " Client (Frontend): https://localhost:$client_port" -ForegroundColor Green +Write-Host " Server (API): https://localhost:$server_port" -ForegroundColor Green +Write-Host "" +Write-Host "Note: Accept the self-signed certificate warning in your browser" -ForegroundColor Yellow +Write-Host "Both services use Kestrel with HTTPS and shared SSL certificates" -ForegroundColor Cyan +Write-Host "" +Write-Host "To view logs: docker-compose logs -f" -ForegroundColor Cyan +Write-Host "To stop: docker-compose down" -ForegroundColor Cyan diff --git a/src/Hosts/web-release/stop.ps1 b/src/Hosts/web-release/stop.ps1 new file mode 100644 index 00000000..7533822a --- /dev/null +++ b/src/Hosts/web-release/stop.ps1 @@ -0,0 +1,47 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Stop the Budget application +.DESCRIPTION + This script stops all services and optionally removes volumes +.PARAMETER RemoveVolumes + Remove all volumes (database data will be lost!) +.EXAMPLE + .\stop.ps1 + .\stop.ps1 -RemoveVolumes +#> + +param( + [switch]$RemoveVolumes +) + +$ErrorActionPreference = "Stop" + +Write-Host "Budget Application - Stopping Services" -ForegroundColor Cyan +Write-Host "======================================" -ForegroundColor Cyan +Write-Host "" + +if ($RemoveVolumes) { + Write-Host "⚠️ WARNING: This will remove all volumes including database data!" -ForegroundColor Red + $confirm = Read-Host "Are you sure? Type 'yes' to confirm" + + if ($confirm -ne "yes") { + Write-Host "Cancelled" -ForegroundColor Yellow + exit 0 + } + + Write-Host "Stopping services and removing volumes..." -ForegroundColor Yellow + docker-compose down -v +} +else { + Write-Host "Stopping services..." -ForegroundColor Cyan + docker-compose down +} + +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to stop services" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "✓ Services stopped successfully" -ForegroundColor Green diff --git a/src/Hosts/web-release/update.ps1 b/src/Hosts/web-release/update.ps1 new file mode 100644 index 00000000..330555a8 --- /dev/null +++ b/src/Hosts/web-release/update.ps1 @@ -0,0 +1,80 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Update Budget application to a new version +.DESCRIPTION + This script pulls new images and restarts services +.PARAMETER Version + Version tag to update to (updates .env file) +.EXAMPLE + .\update.ps1 + .\update.ps1 -Version "1.0.1" +#> + +param( + [string]$Version +) + +$ErrorActionPreference = "Stop" + +Write-Host "Budget Application - Update" -ForegroundColor Cyan +Write-Host "===========================" -ForegroundColor Cyan +Write-Host "" + +# Check if .env exists +if (-not (Test-Path ".env")) { + Write-Host "❌ .env file not found!" -ForegroundColor Red + exit 1 +} + +# Update version in .env if specified +if ($Version) { + Write-Host "Updating version to: $Version" -ForegroundColor Cyan + $env_content = Get-Content ".env" + $updated = $false + + $new_content = $env_content | ForEach-Object { + if ($_ -match "^\s*IMAGE_VERSION\s*=") { + $updated = $true + "IMAGE_VERSION=$Version" + } + else { + $_ + } + } + + if (-not $updated) { + $new_content += "IMAGE_VERSION=$Version" + } + + $new_content | Set-Content ".env" + Write-Host "✓ Version updated in .env" -ForegroundColor Green + Write-Host "" +} + +# Pull new images +Write-Host "Pulling images..." -ForegroundColor Cyan +docker-compose pull + +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to pull images" -ForegroundColor Red + exit 1 +} + +Write-Host "✓ Images pulled successfully" -ForegroundColor Green +Write-Host "" + +# Restart services +Write-Host "Restarting services..." -ForegroundColor Cyan +docker-compose up -d + +if ($LASTEXITCODE -ne 0) { + Write-Host "❌ Failed to restart services" -ForegroundColor Red + exit 1 +} + +Write-Host "" +Write-Host "✓ Update completed successfully!" -ForegroundColor Green +Write-Host "" +Write-Host "Monitor the services with: docker-compose ps" -ForegroundColor Cyan +Write-Host "View logs with: .\logs.ps1 -Follow" -ForegroundColor Cyan diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/FileReadingSetting.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/FileReadingSetting.cs new file mode 100644 index 00000000..c7f8518e --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/FileReadingSetting.cs @@ -0,0 +1,12 @@ +using System.Globalization; +using System.Text; + +namespace NVs.Budget.Infrastructure.Files.CSV.Contracts; + +public record FileReadingSetting( + CultureInfo Culture, + Encoding Encoding, + DateTimeKind DateTimeKind, + IReadOnlyDictionary Fields, + IReadOnlyDictionary Attributes, + IReadOnlyCollection Validation); diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ICsvFileReader.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ICsvFileReader.cs new file mode 100644 index 00000000..5d3f9c50 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ICsvFileReader.cs @@ -0,0 +1,9 @@ +using FluentResults; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Infrastructure.Files.CSV.Contracts; + +public interface ICsvFileReader +{ + IAsyncEnumerable> ReadUntrackedOperations(StreamReader reader, FileReadingSetting config, CancellationToken ct); +}; diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/IReadingSettingsRepository.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/IReadingSettingsRepository.cs new file mode 100644 index 00000000..d5040fc5 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/IReadingSettingsRepository.cs @@ -0,0 +1,12 @@ +using System.Text.RegularExpressions; +using FluentResults; +using NVs.Budget.Application.Contracts.Entities.Accounting; + +namespace NVs.Budget.Infrastructure.Files.CSV.Contracts; + +public interface IReadingSettingsRepository +{ + Task> GetReadingSettingsFor(TrackedBudget budget, CancellationToken ct); + + Task UpdateReadingSettingsFor(TrackedBudget budget, IReadOnlyDictionary settings, CancellationToken ct); +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/NVs.Budget.Infrastructure.Files.CSV.Contracts.csproj b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/NVs.Budget.Infrastructure.Files.CSV.Contracts.csproj new file mode 100644 index 00000000..34de15e8 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/NVs.Budget.Infrastructure.Files.CSV.Contracts.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/ValidationRule.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ValidationRule.cs similarity index 51% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/ValidationRule.cs rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ValidationRule.cs index 89616a0b..26a0a2d9 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/ValidationRule.cs +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Contracts/ValidationRule.cs @@ -1,14 +1,15 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; +namespace NVs.Budget.Infrastructure.Files.CSV.Contracts; public record ValidationRule( - FieldConfiguration FieldConfiguration, + string Pattern, ValidationRule.ValidationCondition Condition, - string Value + string Value, + string ErrorMessage ) { public enum ValidationCondition { Equals, - NotEquals + NotEquals, } } diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/BudgetSpecificSettingsRepositoryShould.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/BudgetSpecificSettingsRepositoryShould.cs new file mode 100644 index 00000000..b11a1d0b --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/BudgetSpecificSettingsRepositoryShould.cs @@ -0,0 +1,83 @@ +using System.Text.RegularExpressions; +using AutoFixture; +using FluentAssertions; +using FluentResults.Extensions.FluentAssertions; +using NVs.Budget.Application.Contracts.Criteria; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; +using NVs.Budget.Utilities.Testing; + +namespace NVs.Budget.Infrastructure.Files.CSV.Tests; + +[Collection(nameof(DatabaseCollectionFixture))] +public class BudgetSpecificSettingsRepositoryShould : IClassFixture +{ + private readonly BudgetSpecificSettingsRepository _repository; + private readonly Fixture _fixture; + + public BudgetSpecificSettingsRepositoryShould(DbContextManager manager) + { + _repository = new BudgetSpecificSettingsRepository(manager.GetSettingsContext()); + _fixture = new Fixture() { Customizations = { new ReadableExpressionsBuilder() } }; + _fixture.Inject(LogbookCriteria.Universal); + } + + [Fact] + public async Task Should_SaveAndRetrieve_FileReadingSettings_Successfully() + { + var budget = _fixture.Create(); + var expectedSettings = _fixture.Create>(); + + var saveResult = await _repository.UpdateReadingSettingsFor(budget, expectedSettings, CancellationToken.None); + saveResult.Should().BeSuccess(); + + var retrievedSettings = await _repository.GetReadingSettingsFor(budget, CancellationToken.None); + retrievedSettings.Should().NotBeNull(); + retrievedSettings.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().BeEquivalentTo(expectedSettings.ToDictionary(x => x.Key.ToString(), x => x.Value)); + } + + [Fact] + public async Task Should_Not_Return_Old_Configs_After_Update() + { + var budget = _fixture.Create(); + var oldSettings = _fixture.Create>(); + var newSettings = _fixture.Create>(); + + await _repository.UpdateReadingSettingsFor(budget, oldSettings, CancellationToken.None); + var firstRetrieval = await _repository.GetReadingSettingsFor(budget, CancellationToken.None); + firstRetrieval.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().BeEquivalentTo(oldSettings.ToDictionary(x => x.Key.ToString(), x => x.Value)); + + + await _repository.UpdateReadingSettingsFor(budget, newSettings, CancellationToken.None); + var secondRetrieval = await _repository.GetReadingSettingsFor(budget, CancellationToken.None); + secondRetrieval.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().BeEquivalentTo(newSettings.ToDictionary(x => x.Key.ToString(), x => x.Value)); + + secondRetrieval.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().NotBeEquivalentTo(oldSettings.ToDictionary(x => x.Key.ToString(), x => x.Value)); + } + + [Fact] + public async Task Should_Retrieve_Settings_Only_For_Assigned_Budget() + { + var budget1 = _fixture.Create(); + var settings1 = _fixture.Create>(); + + var budget2 = _fixture.Create(); + var settings2 = _fixture.Create>(); + + await _repository.UpdateReadingSettingsFor(budget1, settings1, CancellationToken.None); + await _repository.UpdateReadingSettingsFor(budget2, settings2, CancellationToken.None); + + var retrievedSettings1 = await _repository.GetReadingSettingsFor(budget1, CancellationToken.None); + retrievedSettings1.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().BeEquivalentTo(settings1.ToDictionary(x => x.Key.ToString(), x => x.Value)); + + var retrievedSettings2 = await _repository.GetReadingSettingsFor(budget2, CancellationToken.None); + retrievedSettings2.ToDictionary(x => x.Key.ToString(), x => x.Value) + .Should().BeEquivalentTo(settings2.ToDictionary(x => x.Key.ToString(), x => x.Value)); + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/CsvFileReaderShould.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/CsvFileReaderShould.cs new file mode 100644 index 00000000..0c2d24db --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/CsvFileReaderShould.cs @@ -0,0 +1,860 @@ +using System.Globalization; +using System.Text; +using FluentAssertions; +using FluentResults.Extensions.FluentAssertions; +using NMoneys; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Infrastructure.Files.CSV.Tests; + +public class CsvFileReaderShould +{ + private readonly CsvFileReader _reader; + + public CsvFileReaderShould() + { + _reader = new CsvFileReader(); + } + + private static async Task> ToListAsync(IAsyncEnumerable source) + { + var list = new List(); + await foreach (var item in source) + { + list.Add(item); + } + return list; + } + + [Fact] + public async Task ReadSimpleOperationsSuccessfully() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD,Coffee shop + 2024-01-16,200.00,USD,Grocery store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(100.50m, Currency.Usd)); + results[0].Value.Description.Should().Be("Coffee shop"); + + results[1].Value.Timestamp.Should().Be(new DateTime(2024, 1, 16, 0, 0, 0, DateTimeKind.Utc)); + results[1].Value.Amount.Should().Be(new Money(200.00m, Currency.Usd)); + results[1].Value.Description.Should().Be("Grocery store"); + } + + [Fact] + public async Task ReadOperationsWithPatternCombinations() + { + // Arrange + var csv = """ + 2024-01-15,100,50,USD,Coffee,shop + 2024-01-16,200,00,EUR,Grocery,store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}.{2}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{3}", + [nameof(UnregisteredOperation.Description)] = "{4} {5}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + + results[0].Value.Amount.Should().Be(new Money(100.50m, Currency.Usd)); + results[0].Value.Description.Should().Be("Coffee shop"); + + results[1].Value.Amount.Should().Be(new Money(200.00m, Currency.Eur)); + results[1].Value.Description.Should().Be("Grocery store"); + } + + [Fact] + public async Task ReadOperationsWithAttributes() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD,Coffee shop,Card,Personal + 2024-01-16,200.00,USD,Grocery store,Cash,Business + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary + { + ["PaymentMethod"] = "{4}", + ["Category"] = "{5}" + }, + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + + results[0].Value.Attributes.Should().NotBeNull(); + results[0].Value.Attributes!["PaymentMethod"].Should().Be("Card"); + results[0].Value.Attributes!["Category"].Should().Be("Personal"); + + results[1].Value.Attributes.Should().NotBeNull(); + results[1].Value.Attributes!["PaymentMethod"].Should().Be("Cash"); + results[1].Value.Attributes!["Category"].Should().Be("Business"); + } + + [Fact] + public async Task SkipRowsThatFailValidation_Equals() + { + // Arrange + var csv = """ + header,row,to,skip + 2024-01-15,100.50,USD,Coffee shop + 2024-01-16,200.00,USD,Grocery store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: new[] + { + new ValidationRule( + Pattern: "{0}", + Condition: ValidationRule.ValidationCondition.NotEquals, + Value: "header", + ErrorMessage: "" + ) + } + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + } + + [Fact] + public async Task ReturnFailureForRowsWithValidationError() + { + // Arrange + var csv = """ + invalid,100.50,USD,Coffee shop + 2024-01-16,200.00,USD,Grocery store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: new[] + { + new ValidationRule( + Pattern: "{0}", + Condition: ValidationRule.ValidationCondition.NotEquals, + Value: "invalid", + ErrorMessage: "Invalid row detected" + ) + } + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results[0].Should().BeFailure(); + results[0].Errors.Should().ContainSingle(e => e.Message == "Unable to parse row!"); + + results[1].Should().BeSuccess(); + } + + [Fact] + public async Task ReturnFailureForInvalidDateFormat() + { + // Arrange + var csv = """ + not-a-date,100.50,USD,Coffee shop + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeFailure(); + results[0].Errors.Should().ContainSingle(e => e.Message == "Unable to parse row!"); + } + + [Fact] + public async Task ReturnFailureForInvalidAmountFormat() + { + // Arrange + var csv = """ + 2024-01-15,not-a-number,USD,Coffee shop + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeFailure(); + results[0].Errors.Should().ContainSingle(e => e.Message == "Unable to parse row!"); + } + + [Fact] + public async Task ReturnFailureForInvalidCurrencyCode() + { + // Arrange + var csv = """ + 2024-01-15,100.50,INVALID,Coffee shop + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeFailure(); + results[0].Errors.Should().ContainSingle(e => e.Message == "Unable to parse row!"); + } + + [Fact] + public async Task ReturnFailureForMissingRequiredField() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD,Coffee shop + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + // Missing Timestamp field + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeFailure(); + results[0].Errors[0].Reasons.Should().ContainSingle(e => e.Message.Contains("No field options provided")); + } + + [Fact] + public async Task HandleEmptyRows() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD,Coffee shop + + 2024-01-16,200.00,USD,Grocery store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + } + + [Fact] + public async Task ReadOperationsWithLocalDateTimeKind() + { + // Arrange - testing with Local DateTimeKind + var csv = """ + 2024-01-15,1234.56,EUR,Coffee shop + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Local, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeSuccess(); + results[0].Value.Timestamp.Kind.Should().Be(DateTimeKind.Local); + results[0].Value.Amount.Should().Be(new Money(1234.56m, Currency.Eur)); + } + + [Fact] + public async Task HandleComplexValidationRules() + { + // Arrange + var csv = """ + expense,2024-01-15,100.50,USD,Coffee shop + income,2024-01-16,500.00,USD,Salary + expense,2024-01-17,200.00,USD,Grocery store + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{1}", + [nameof(UnregisteredOperation.Amount)] = "{2}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{3}", + [nameof(UnregisteredOperation.Description)] = "{4}" + }, + Attributes: new Dictionary + { + ["Type"] = "{0}" + }, + Validation: new[] + { + new ValidationRule( + Pattern: "{0}", + Condition: ValidationRule.ValidationCondition.Equals, + Value: "expense", + ErrorMessage: "" + ) + } + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); // Only expense rows should be processed + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Description.Should().Be("Coffee shop"); + results[1].Value.Description.Should().Be("Grocery store"); + } + + [Fact] + public async Task IncludeRowNumberInErrorMetadata() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD,Coffee shop + invalid-date,200.00,USD,Grocery store + 2024-01-17,300.00,USD,Restaurant + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(3); + results[0].Should().BeSuccess(); + results[1].Should().BeFailure(); + results[1].Errors[0].Metadata.Should().ContainKey("row"); + results[1].Errors[0].Metadata["row"].Should().Be(2); + results[2].Should().BeSuccess(); + } + + [Fact] + public async Task HandlePatternWithoutPlaceholders() + { + // Arrange + var csv = """ + 2024-01-15,100.50,USD + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "Default description" // No placeholder + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().ContainSingle(); + results[0].Should().BeSuccess(); + results[0].Value.Description.Should().Be("Default description"); + } + + [Fact] + public async Task ReadOperationsWithFrenchCulture() + { + // Arrange - French culture (fr-FR): uses semicolon delimiter, comma as decimal separator + var csv = """ + 2024-01-15;1234,56;EUR;Café + 2024-01-16;200,00;EUR;Résumé financier + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: new CultureInfo("fr-FR"), + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(1234.56m, Currency.Eur)); + results[0].Value.Description.Should().Be("Café"); + results[1].Value.Amount.Should().Be(new Money(200.00m, Currency.Eur)); + results[1].Value.Description.Should().Be("Résumé financier"); + } + + [Fact] + public async Task ReadOperationsWithRussianCulture() + { + // Arrange - Russian culture (ru-RU): uses semicolon delimiter, comma as decimal separator + var csv = """ + 2024-01-15;100,50;EUR;Магазин + 2024-01-16;250,75;EUR;Ресторан + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: new CultureInfo("ru-RU"), + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(100.50m, Currency.Eur)); + results[0].Value.Description.Should().Be("Магазин"); + results[1].Value.Amount.Should().Be(new Money(250.75m, Currency.Eur)); + results[1].Value.Description.Should().Be("Ресторан"); + } + + [Fact] + public async Task ReadOperationsWithJapaneseCulture() + { + // Arrange - Japanese culture (ja-JP): uses comma delimiter, period as decimal separator + var csv = """ + 2024-01-15,1000.00,JPY,コーヒー + 2024-01-16,5000.50,JPY,レストラン + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: new CultureInfo("ja-JP"), + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(1000m, Currency.Jpy)); + results[0].Value.Description.Should().Be("コーヒー"); + results[1].Value.Amount.Should().Be(new Money(5000.50m, Currency.Jpy)); + results[1].Value.Description.Should().Be("レストラン"); + } + + [Fact] + public async Task ReadOperationsWithUTF16Encoding() + { + // Arrange - UTF-16 encoded file + var csv = """ + 2024-01-15,100.50,USD,Coffee shop + 2024-01-16,200.00,USD,Store + """; + var stream = CreateStreamReader(csv, Encoding.Unicode); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.Unicode, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + } + + [Fact] + public async Task ReadOperationsWithUTF32Encoding() + { + // Arrange - UTF-32 encoded file with emoji + var csv = """ + 2024-01-15,100.50,USD,Coffee ☕ + 2024-01-16,200.00,USD,Restaurant 🍽️ + """; + var stream = CreateStreamReader(csv, Encoding.UTF32); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: Encoding.UTF32, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Description.Should().Be("Coffee ☕"); + results[1].Value.Description.Should().Be("Restaurant 🍽️"); + } + + [Fact] + public async Task ReadOperationsWithLatin1Encoding() + { + // Arrange - Latin1 (ISO-8859-1) encoding with special characters + var csv = """ + 2024-01-15,100.50,EUR,Café résumé + 2024-01-16,200.00,EUR,Naïve émigré + """; + var encoding = Encoding.GetEncoding("ISO-8859-1"); + var stream = CreateStreamReader(csv, encoding); + + var config = new FileReadingSetting( + Culture: CultureInfo.InvariantCulture, + Encoding: encoding, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Description.Should().Be("Café résumé"); + results[1].Value.Description.Should().Be("Naïve émigré"); + } + + [Fact] + public async Task ReadOperationsWithChineseCulture() + { + // Arrange - Chinese culture (zh-CN): uses comma delimiter, period as decimal separator + var csv = """ + 2024-01-15,1234.56,CNY,咖啡店 + 2024-01-16,200.00,CNY,超市 + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: new CultureInfo("zh-CN"), + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(1234.56m, Currency.Cny)); + results[0].Value.Description.Should().Be("咖啡店"); + results[1].Value.Amount.Should().Be(new Money(200.00m, Currency.Cny)); + results[1].Value.Description.Should().Be("超市"); + } + + [Fact] + public async Task ReadOperationsWithArabicCulture() + { + // Arrange - English (US) culture with Arabic text (demonstrating RTL text support) + var csv = """ + 2024-01-15,1234.50,USD,مقهى + 2024-01-16,200.00,USD,مطعم + """; + var stream = CreateStreamReader(csv); + + var config = new FileReadingSetting( + Culture: new CultureInfo("en-US"), + Encoding: Encoding.UTF8, + DateTimeKind: DateTimeKind.Utc, + Fields: new Dictionary + { + [nameof(UnregisteredOperation.Timestamp)] = "{0}", + [nameof(UnregisteredOperation.Amount)] = "{1}", + [nameof(UnregisteredOperation.Amount.CurrencyCode)] = "{2}", + [nameof(UnregisteredOperation.Description)] = "{3}" + }, + Attributes: new Dictionary(), + Validation: Array.Empty() + ); + + // Act + var results = await ToListAsync(_reader.ReadUntrackedOperations(stream, config, CancellationToken.None)); + + // Assert + results.Should().HaveCount(2); + results.Should().AllSatisfy(r => r.Should().BeSuccess()); + results[0].Value.Timestamp.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0, DateTimeKind.Utc)); + results[0].Value.Amount.Should().Be(new Money(1234.50m, Currency.Usd)); + results[0].Value.Description.Should().Be("مقهى"); + results[1].Value.Amount.Should().Be(new Money(200.00m, Currency.Usd)); + results[1].Value.Description.Should().Be("مطعم"); + } + + private static StreamReader CreateStreamReader(string content, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + var bytes = encoding.GetBytes(content); + var stream = new MemoryStream(bytes); + return new StreamReader(stream, encoding); + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/DbContextManager.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/DbContextManager.cs new file mode 100644 index 00000000..2113f34e --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/DbContextManager.cs @@ -0,0 +1,40 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; +using Testcontainers.PostgreSql; + +namespace NVs.Budget.Infrastructure.Files.CSV.Tests; + +[CollectionDefinition(nameof(DatabaseCollectionFixture))] +public class DatabaseCollectionFixture : ICollectionFixture; + +[UsedImplicitly] +public class DbContextManager : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder().Build(); + + public async Task InitializeAsync() + { + await _postgreSqlContainer.StartAsync(); + + var context = GetSettingsContext(); + await context.Database.MigrateAsync(); + } + + internal SettingsContext GetSettingsContext() + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseNpgsql(_postgreSqlContainer.GetConnectionString() + ";Include Error Detail=true") + .EnableSensitiveDataLogging() + .EnableDetailedErrors(); + + var context = new SettingsContext(optionsBuilder.Options); + return context; + } + + + public Task DisposeAsync() + { + return _postgreSqlContainer.DisposeAsync().AsTask(); + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/GlobalUsings.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/GlobalUsings.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/GlobalUsings.cs rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/GlobalUsings.cs diff --git a/src/Controllers/NVs.Budget.Controllers.Console.IO.Tests/NVs.Budget.Controllers.Console.IO.Tests.csproj b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/NVs.Budget.Infrastructure.Files.CSV.Tests.csproj similarity index 66% rename from src/Controllers/NVs.Budget.Controllers.Console.IO.Tests/NVs.Budget.Controllers.Console.IO.Tests.csproj rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/NVs.Budget.Infrastructure.Files.CSV.Tests.csproj index d54e6539..241269ed 100644 --- a/src/Controllers/NVs.Budget.Controllers.Console.IO.Tests/NVs.Budget.Controllers.Console.IO.Tests.csproj +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV.Tests/NVs.Budget.Infrastructure.Files.CSV.Tests.csproj @@ -11,27 +11,26 @@ - - - - - + + + + - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvExtensions.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvExtensions.cs new file mode 100644 index 00000000..94f15d60 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; +using NVs.Budget.Infrastructure.Persistence.EF.Common; +using NVs.Budget.Infrastructure.Persistence.EF.Context; + +[assembly:InternalsVisibleTo("NVs.Budget.Infrastructure.Files.CSV.Tests")] +namespace NVs.Budget.Infrastructure.Files.CSV; + +public static class CsvExtensions +{ + public static IServiceCollection AddCsvFiles(this IServiceCollection services, string connectionString) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + services.AddDbContext(o => o.UseNpgsql(connectionString)) + .AddTransient>() + .AddScoped() + .AddScoped(); + return services; + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvFileReader.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvFileReader.cs new file mode 100644 index 00000000..3dc29711 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/CsvFileReader.cs @@ -0,0 +1,260 @@ +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using CsvHelper; +using CsvHelper.Configuration; +using FluentResults; +using NMoneys; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; +using NVs.Budget.Infrastructure.Files.CSV.Errors; + +namespace NVs.Budget.Infrastructure.Files.CSV; + +internal partial class CsvFileReader : ICsvFileReader +{ + private static readonly Regex CellsIndexPattern = GenerateCellIndexPattern(); + + public async IAsyncEnumerable> ReadUntrackedOperations( + StreamReader reader, + FileReadingSetting config, + [EnumeratorCancellation] CancellationToken ct) + { + var csvConfig = new CsvConfiguration(config.Culture) + { + HasHeaderRecord = false, + MissingFieldFound = null + }; + + var parser = new CsvParser(reader, csvConfig, true); + var rowNumber = 0; + + while (await parser.ReadAsync()) + { + ct.ThrowIfCancellationRequested(); + rowNumber++; + + // Skip empty rows + if (string.IsNullOrWhiteSpace(parser.RawRecord)) + { + continue; + } + + // Validate row + var validationResult = ValidateRow(parser, config.Validation, rowNumber); + if (validationResult.IsFailed) + { + yield return validationResult.ToResult(); + continue; + } + + if (!validationResult.Value) + { + // Row doesn't meet validation criteria but it's not an error (e.g., it's a header row) + continue; + } + + // Parse row + var operationResult = ParseRow(parser, config, rowNumber); + yield return operationResult; + } + } + + private Result ValidateRow(IParser parser, IReadOnlyCollection validationRules, int rowNumber) + { + if (validationRules == null || validationRules.Count == 0) + { + return true; + } + + foreach (var rule in validationRules) + { + var valueResult = EvaluatePattern(parser, rule.Pattern); + if (valueResult.IsFailed) + { + return Result.Fail(new RowNotParsedError(rowNumber, valueResult.Errors)); + } + + var matches = rule.Condition switch + { + ValidationRule.ValidationCondition.Equals => valueResult.Value == rule.Value, + ValidationRule.ValidationCondition.NotEquals => valueResult.Value != rule.Value, + _ => throw new ArgumentOutOfRangeException(nameof(rule.Condition)) + }; + + if (!matches) + { + if (!string.IsNullOrEmpty(rule.ErrorMessage)) + { + return Result.Fail(new RowNotParsedError(rowNumber, + new List { new ValidationFailedError(rule.ErrorMessage) })); + } + return false; + } + } + + return true; + } + + private Result ParseRow(IParser parser, FileReadingSetting config, int rowNumber) + { + // Parse timestamp + var timestampResult = ParseField(parser, config.Fields, nameof(UnregisteredOperation.Timestamp), + s => DateTime.SpecifyKind(DateTime.Parse(s, config.Culture), config.DateTimeKind)); + if (timestampResult.IsFailed) + { + return BuildParseError(rowNumber, timestampResult.Errors); + } + + // Parse currency code + var currencyResult = ParseField(parser, config.Fields, nameof(UnregisteredOperation.Amount.CurrencyCode), Currency.Get); + if (currencyResult.IsFailed) + { + return BuildParseError(rowNumber, currencyResult.Errors); + } + + // Parse amount + var moneyResult = ParseField(parser, config.Fields, nameof(UnregisteredOperation.Amount), + m => new Money(decimal.Parse(m, NumberStyles.Any, config.Culture), currencyResult.Value)); + if (moneyResult.IsFailed) + { + return BuildParseError(rowNumber, moneyResult.Errors); + } + + // Parse description + var descriptionResult = ParseField(parser, config.Fields, nameof(UnregisteredOperation.Description), s => s); + if (descriptionResult.IsFailed) + { + return BuildParseError(rowNumber, descriptionResult.Errors); + } + + // Parse attributes + var attributesResult = ParseAttributes(parser, config.Attributes); + if (attributesResult.IsFailed) + { + return BuildParseError(rowNumber, attributesResult.Errors); + } + + return new UnregisteredOperation( + timestampResult.Value, + moneyResult.Value, + descriptionResult.Value, + attributesResult.Value + ); + } + + private Result?> ParseAttributes(IParser parser, IReadOnlyDictionary attributePatterns) + { + if (attributePatterns == null || attributePatterns.Count == 0) + { + return Result.Ok?>(null); + } + + var attributes = new Dictionary(); + + foreach (var (name, pattern) in attributePatterns) + { + var valueResult = EvaluatePattern(parser, pattern); + if (valueResult.IsFailed) + { + return Result.Fail?>(new AttributeParsingError(name)); + } + + attributes.Add(name, valueResult.Value); + } + + return Result.Ok?>(attributes); + } + + private Result ParseField(IParser parser, IReadOnlyDictionary fields, string fieldName, Func convertFn) + { + if (!fields.TryGetValue(fieldName, out var pattern)) + { + return Result.Fail(new NoFieldOptionsProvidedFor(fieldName)); + } + + var rawValueResult = EvaluatePattern(parser, pattern); + if (rawValueResult.IsFailed) + { + return rawValueResult.ToResult(); + } + + try + { + return convertFn(rawValueResult.Value); + } + catch (Exception e) + { + var error = new ConversionError(e); + error.Metadata.Add(nameof(fieldName), fieldName); + return Result.Fail(error); + } + } + + private Result EvaluatePattern(IParser parser, string pattern) + { + var usedCells = CellsIndexPattern.Matches(pattern) + .Select(m => (match: m, index: int.Parse(m.Groups[1].Value))) + .ToList(); + + if (usedCells.Count == 0) + { + // No placeholders, return the pattern as-is + return pattern; + } + + try + { + var values = usedCells + .Select(m => m.index) + .Distinct() + .Select(i => (index: i, value: parser[i])) + .ToDictionary(v => v.index, v => v.value); + + var matchIndex = 0; + var strpos = 0; + var builder = new StringBuilder(); + + while (matchIndex < usedCells.Count) + { + var match = usedCells[matchIndex]; + + // Append any text before the placeholder + if (strpos < match.match.Index) + { + builder.Append(pattern, strpos, match.match.Index - strpos); + strpos = match.match.Index; + } + + // Append the cell value + builder.Append(values[match.index]); + strpos += match.match.Length; + matchIndex++; + } + + // Append any remaining text after the last placeholder + if (strpos < pattern.Length) + { + builder.Append(pattern, strpos, pattern.Length - strpos); + } + + return builder.ToString(); + } + catch (Exception e) + { + var error = new ConversionError(e); + error.Metadata.Add("Pattern", pattern); + return Result.Fail(error); + } + } + + private Result BuildParseError(int rowNumber, List errors) + { + return Result.Fail(new RowNotParsedError(rowNumber, errors)); + } + + [GeneratedRegex(@"\{(\d+)\}", RegexOptions.Compiled)] + private static partial Regex GenerateCellIndexPattern(); +} + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/AttributeParsingError.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/AttributeParsingError.cs similarity index 82% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/AttributeParsingError.cs rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/AttributeParsingError.cs index c912abbb..99ea9b9f 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/AttributeParsingError.cs +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/AttributeParsingError.cs @@ -1,6 +1,6 @@ using FluentResults; -namespace NVs.Budget.Infrastructure.IO.Console.Input.Errors; +namespace NVs.Budget.Infrastructure.Files.CSV.Errors; internal class AttributeParsingError(string attributeName) : IError { @@ -8,3 +8,4 @@ internal class AttributeParsingError(string attributeName) : IError public Dictionary Metadata { get; } = new(); public List Reasons { get; } = new(); } + diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ExceptionalError.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ExceptionalError.cs new file mode 100644 index 00000000..fb516566 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ExceptionalError.cs @@ -0,0 +1,10 @@ +using FluentResults; + +namespace NVs.Budget.Infrastructure.Files.CSV.Errors; + +internal class ConversionError(Exception exception) : IError +{ + public string Message => exception.Message; + public Dictionary Metadata { get; } = new() { { nameof(exception), exception.ToString() } }; + public List Reasons { get; } = new(); +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/NoFieldOptionsProvidedFor.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/NoFieldOptionsProvidedFor.cs similarity index 77% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/NoFieldOptionsProvidedFor.cs rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/NoFieldOptionsProvidedFor.cs index 4c7db649..0c50d5c2 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/NoFieldOptionsProvidedFor.cs +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/NoFieldOptionsProvidedFor.cs @@ -1,6 +1,6 @@ using FluentResults; -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader.Errors; +namespace NVs.Budget.Infrastructure.Files.CSV.Errors; internal class NoFieldOptionsProvidedFor(string name) : IError { @@ -8,3 +8,4 @@ internal class NoFieldOptionsProvidedFor(string name) : IError public Dictionary Metadata { get; } = new(); public List Reasons { get; } = new(); } + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/RowNotParsedError.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/RowNotParsedError.cs similarity index 83% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/RowNotParsedError.cs rename to src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/RowNotParsedError.cs index 4363c057..34e67c3e 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Errors/RowNotParsedError.cs +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/RowNotParsedError.cs @@ -1,6 +1,6 @@ using FluentResults; -namespace NVs.Budget.Infrastructure.IO.Console.Input.Errors; +namespace NVs.Budget.Infrastructure.Files.CSV.Errors; internal class RowNotParsedError(int row, List reasons) : IError { @@ -8,3 +8,4 @@ internal class RowNotParsedError(int row, List reasons) : IError public Dictionary Metadata { get; } = new() { { nameof(row), row } }; public List Reasons { get; } = reasons.ToList(); } + diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ValidationFailedError.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ValidationFailedError.cs new file mode 100644 index 00000000..01b1f15f --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Errors/ValidationFailedError.cs @@ -0,0 +1,11 @@ +using FluentResults; + +namespace NVs.Budget.Infrastructure.Files.CSV.Errors; + +internal class ValidationFailedError(string errorMessage) : IError +{ + public string Message { get; } = errorMessage; + public Dictionary Metadata { get; } = new(); + public List Reasons { get; } = new(); +} + diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/NVs.Budget.Infrastructure.Files.CSV.csproj b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/NVs.Budget.Infrastructure.Files.CSV.csproj new file mode 100644 index 00000000..ca30552d --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/NVs.Budget.Infrastructure.Files.CSV.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.Designer.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.Designer.cs new file mode 100644 index 00000000..be4382cc --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.Designer.cs @@ -0,0 +1,131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Files.CSV; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Migrations +{ + [DbContext(typeof(SettingsContext))] + [Migration("20250628192322_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("csv_settings") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "validation_condition", new[] { "equal", "not_equal" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.StoredCsvFileReadingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("FileNamePattern") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CsvFileReadingSettings", "csv_settings"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.StoredCsvFileReadingSettings", b => + { + b.OwnsOne("NVs.Budget.Infrastructure.Files.CSV.StoredSettings", "Settings", b1 => + { + b1.Property("StoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b1.Property("Attributes") + .IsRequired() + .HasColumnType("text"); + + b1.Property("CultureCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("EncodingName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Fields") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("StoredCsvFileReadingSettingsId"); + + b1.ToTable("CsvFileReadingSettings", "csv_settings"); + + b1.ToJson("Settings"); + + b1.WithOwner() + .HasForeignKey("StoredCsvFileReadingSettingsId"); + + b1.OwnsMany("NVs.Budget.Infrastructure.Files.CSV.Contracts.ValidationRule", "Validation", b2 => + { + b2.Property("StoredSettingsStoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Condition") + .HasColumnType("integer"); + + b2.Property("ErrorMessage") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("StoredSettingsStoredCsvFileReadingSettingsId", "Id"); + + b2.ToTable("CsvFileReadingSettings", "csv_settings"); + + b2.WithOwner() + .HasForeignKey("StoredSettingsStoredCsvFileReadingSettingsId"); + }); + + b1.Navigation("Validation"); + }); + + b.Navigation("Settings") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.cs new file mode 100644 index 00000000..8a269777 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250628192322_Initial.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "csv_settings"); + + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:validation_condition", "equal,not_equal"); + + migrationBuilder.CreateTable( + name: "CsvFileReadingSettings", + schema: "csv_settings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + BudgetId = table.Column(type: "uuid", nullable: false), + FileNamePattern = table.Column(type: "text", nullable: false), + Deleted = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Settings = table.Column(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CsvFileReadingSettings", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CsvFileReadingSettings", + schema: "csv_settings"); + } + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.Designer.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.Designer.cs new file mode 100644 index 00000000..40ad9678 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.Designer.cs @@ -0,0 +1,134 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Migrations +{ + [DbContext(typeof(SettingsContext))] + [Migration("20250630190345_DateTimeKind")] + partial class DateTimeKind + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("csv_settings") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "date_time_kind", new[] { "unspecified", "utc", "local" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "validation_condition", new[] { "equals", "not_equals" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredCsvFileReadingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("FileNamePattern") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CsvFileReadingSettings", "csv_settings"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredCsvFileReadingSettings", b => + { + b.OwnsOne("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredSettings", "Settings", b1 => + { + b1.Property("StoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b1.Property("Attributes") + .IsRequired() + .HasColumnType("text"); + + b1.Property("CultureCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DateTimeKind") + .HasColumnType("integer"); + + b1.Property("EncodingName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Fields") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("StoredCsvFileReadingSettingsId"); + + b1.ToTable("CsvFileReadingSettings", "csv_settings"); + + b1.ToJson("Settings"); + + b1.WithOwner() + .HasForeignKey("StoredCsvFileReadingSettingsId"); + + b1.OwnsMany("NVs.Budget.Infrastructure.Files.CSV.Contracts.ValidationRule", "Validation", b2 => + { + b2.Property("StoredSettingsStoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Condition") + .HasColumnType("integer"); + + b2.Property("ErrorMessage") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("StoredSettingsStoredCsvFileReadingSettingsId", "Id"); + + b2.ToTable("CsvFileReadingSettings", "csv_settings"); + + b2.WithOwner() + .HasForeignKey("StoredSettingsStoredCsvFileReadingSettingsId"); + }); + + b1.Navigation("Validation"); + }); + + b.Navigation("Settings") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.cs new file mode 100644 index 00000000..04b466c1 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/20250630190345_DateTimeKind.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Migrations +{ + /// + public partial class DateTimeKind : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:date_time_kind", "unspecified,utc,local"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .OldAnnotation("Npgsql:Enum:date_time_kind", "unspecified,utc,local"); + } + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/SettingsContextModelSnapshot.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/SettingsContextModelSnapshot.cs new file mode 100644 index 00000000..432fc0c0 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Migrations/SettingsContextModelSnapshot.cs @@ -0,0 +1,131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Migrations +{ + [DbContext(typeof(SettingsContext))] + partial class SettingsContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("csv_settings") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "date_time_kind", new[] { "unspecified", "utc", "local" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "validation_condition", new[] { "equals", "not_equals" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredCsvFileReadingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("FileNamePattern") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("CsvFileReadingSettings", "csv_settings"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredCsvFileReadingSettings", b => + { + b.OwnsOne("NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings.StoredSettings", "Settings", b1 => + { + b1.Property("StoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b1.Property("Attributes") + .IsRequired() + .HasColumnType("text"); + + b1.Property("CultureCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DateTimeKind") + .HasColumnType("integer"); + + b1.Property("EncodingName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Fields") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("StoredCsvFileReadingSettingsId"); + + b1.ToTable("CsvFileReadingSettings", "csv_settings"); + + b1.ToJson("Settings"); + + b1.WithOwner() + .HasForeignKey("StoredCsvFileReadingSettingsId"); + + b1.OwnsMany("NVs.Budget.Infrastructure.Files.CSV.Contracts.ValidationRule", "Validation", b2 => + { + b2.Property("StoredSettingsStoredCsvFileReadingSettingsId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Condition") + .HasColumnType("integer"); + + b2.Property("ErrorMessage") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("StoredSettingsStoredCsvFileReadingSettingsId", "Id"); + + b2.ToTable("CsvFileReadingSettings", "csv_settings"); + + b2.WithOwner() + .HasForeignKey("StoredSettingsStoredCsvFileReadingSettingsId"); + }); + + b1.Navigation("Validation"); + }); + + b.Navigation("Settings") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/BudgetSpecificSettingsRepository.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/BudgetSpecificSettingsRepository.cs new file mode 100644 index 00000000..3816e31a --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/BudgetSpecificSettingsRepository.cs @@ -0,0 +1,73 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using FluentResults; +using Microsoft.EntityFrameworkCore; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Extensions; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +internal class BudgetSpecificSettingsRepository(SettingsContext context) : IReadingSettingsRepository +{ + private static readonly IReadOnlyDictionary Empty = new Dictionary().AsReadOnly(); + + public async Task> GetReadingSettingsFor(TrackedBudget budget, CancellationToken ct) => + await context.CsvFileReadingSettings + .Where(o => o.BudgetId == budget.Id && o.Deleted == false) + .ToDictionaryAsync( + x => new Regex(x.FileNamePattern), + x => new FileReadingSetting( + CultureInfo.GetCultureInfo(x.Settings.CultureCode), + Encoding.GetEncoding(x.Settings.EncodingName), + x.Settings.DateTimeKind, + x.Settings.Fields.AsReadOnly(), + x.Settings.Attributes.AsReadOnly(), + x.Settings.Validation.AsReadOnly() + ), + ct); + + + public async Task UpdateReadingSettingsFor(TrackedBudget budget, IReadOnlyDictionary settings, CancellationToken ct) + { + try + { + await using var transaction = await context.Database.BeginTransactionAsync(ct); + + var targets = await context.CsvFileReadingSettings.Where(x => x.BudgetId == budget.Id && x.Deleted == false).ToListAsync(ct); + foreach (var target in targets) + { + target.Deleted = true; + target.UpdatedAt = DateTime.UtcNow; + } + + await context.CsvFileReadingSettings.AddRangeAsync(settings.Select(s => new StoredCsvFileReadingSettings() + { + BudgetId = budget.Id, + FileNamePattern = s.Key.ToString(), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Settings = new() + { + CultureCode = s.Value.Culture.Name, + EncodingName = s.Value.Encoding.WebName, + DateTimeKind = s.Value.DateTimeKind, + Attributes = new(s.Value.Attributes), + Fields = new(s.Value.Fields), + Validation = new(s.Value.Validation), + } + } + ), ct); + + await context.SaveChangesAsync(ct); + await transaction.CommitAsync(ct); + } + catch (Exception ex) + { + return Result.Fail(new ExceptionalError(ex.WithBudgetId(budget))); + } + + return Result.Ok(); + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContext.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContext.cs new file mode 100644 index 00000000..092e7b1f --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContext.cs @@ -0,0 +1,41 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +internal class SettingsContext(DbContextOptions options) : DbContext(options) +{ + private static readonly JsonSerializerOptions JsonOpts = new() { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public DbSet CsvFileReadingSettings { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("csv_settings"); + + modelBuilder.UseIdentityByDefaultColumns(); + modelBuilder.HasPostgresEnum(); + modelBuilder.HasPostgresEnum(); + + modelBuilder.Entity() + .OwnsOne(s => s.Settings, d => + { + d.ToJson(); + d.Property(s => s.Attributes).HasConversion( + v => JsonSerializer.Serialize(v, JsonOpts), + v => JsonSerializer.Deserialize>(v, JsonOpts) ?? new() + ); + d.Property(s => s.Fields).HasConversion( + v => JsonSerializer.Serialize(v, JsonOpts), + v => JsonSerializer.Deserialize>(v, JsonOpts) ?? new() + ); + d.OwnsMany(s => s.Validation); + }); + } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContextDesignTimeDbFactory.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContextDesignTimeDbFactory.cs new file mode 100644 index 00000000..114c7926 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/SettingsContextDesignTimeDbFactory.cs @@ -0,0 +1,5 @@ +using NVs.Budget.Infrastructure.Persistence.EF.Common; + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +internal class SettingsContextDesignTimeDbFactory : DesignTimeContextFactory; diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredCsvFileReadingSettings.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredCsvFileReadingSettings.cs new file mode 100644 index 00000000..3fdb3524 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredCsvFileReadingSettings.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +internal class StoredCsvFileReadingSettings +{ + [Key] public Guid Id { get; init; } + public Guid BudgetId { get; init; } + public string FileNamePattern { get; init; } = string.Empty; + public StoredSettings Settings { get; set; } = StoredSettings.Invalid; + public bool Deleted { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } +} diff --git a/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredSettings.cs b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredSettings.cs new file mode 100644 index 00000000..64f256c4 --- /dev/null +++ b/src/Infrastructure/Files/NVs.Budget.Infrastructure.Files.CSV/Persistence/Settings/StoredSettings.cs @@ -0,0 +1,15 @@ +using NVs.Budget.Infrastructure.Files.CSV.Contracts; + +namespace NVs.Budget.Infrastructure.Files.CSV.Persistence.Settings; + +internal class StoredSettings +{ + public static readonly StoredSettings Invalid = new(); + + public string CultureCode { get; init; } = string.Empty; + public string EncodingName { get; init; } = string.Empty; + public Dictionary Fields { get; init; } = new(); + public Dictionary Attributes { get; init; } = new(); + public List Validation {get; init;} = new(); + public DateTimeKind DateTimeKind { get; init; } = DateTimeKind.Local; +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/Criteria/ICriteriaParser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/Criteria/ICriteriaParser.cs deleted file mode 100644 index 495e7d83..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/Criteria/ICriteriaParser.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Linq.Expressions; -using FluentResults; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.Criteria; - -public interface ICriteriaParser -{ - Result>> TryParsePredicate(string expression, string paramName); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ICsvReadingOptionsReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ICsvReadingOptionsReader.cs deleted file mode 100644 index 475d6d73..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ICsvReadingOptionsReader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface ICsvReadingOptionsReader -{ - Task> ReadFrom(StreamReader reader, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IInputStreamProvider.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IInputStreamProvider.cs deleted file mode 100644 index 8bc7c2dc..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IInputStreamProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface IInputStreamProvider -{ - Task> GetInput(string name = ""); - Task ReleaseStreamsAsync(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ILogbookCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ILogbookCriteriaReader.cs deleted file mode 100644 index 9da2a7db..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ILogbookCriteriaReader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface ILogbookCriteriaReader -{ - Task> ReadFrom(StreamReader reader, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IOperationsReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IOperationsReader.cs deleted file mode 100644 index b0eca7ce..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/IOperationsReader.cs +++ /dev/null @@ -1,12 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface IOperationsReader -{ - IAsyncEnumerable> ReadUnregisteredOperations(StreamReader input, SpecificCsvFileReadingOptions options, CancellationToken ct); - - IAsyncEnumerable> ReadTrackedOperation(StreamReader input, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITaggingCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITaggingCriteriaReader.cs deleted file mode 100644 index 113d84af..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITaggingCriteriaReader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface ITaggingCriteriaReader -{ - IAsyncEnumerable> ReadFrom(StreamReader reader, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransferCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransferCriteriaReader.cs deleted file mode 100644 index 5f36f834..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransferCriteriaReader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface ITransferCriteriaReader -{ - IAsyncEnumerable> ReadFrom(StreamReader reader, CancellationToken ct); -} \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransfersReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransfersReader.cs deleted file mode 100644 index 529fb735..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Input/ITransfersReader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -public interface ITransfersReader -{ - IAsyncEnumerable> ReadUnregisteredTransfers(StreamReader input, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/NVs.Budget.Infrastructure.IO.Console.Contracts.csproj b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/NVs.Budget.Infrastructure.IO.Console.Contracts.csproj deleted file mode 100644 index 1aacb299..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/NVs.Budget.Infrastructure.IO.Console.Contracts.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - enable - NVs.Budget.Infrastructure.IO.Console - - - - - - - - - - - - diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvFileReadingOptions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvFileReadingOptions.cs deleted file mode 100644 index 8ef6d827..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvFileReadingOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections; -using System.Globalization; - -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public class CsvFileReadingOptions( - IDictionary configurations, - CultureInfo culture, - DateTimeKind dateTimeKind = DateTimeKind.Local, - IReadOnlyDictionary? attributesConfiguration = null, - IReadOnlyDictionary? validationRules = null -) : IReadOnlyDictionary -{ - public bool ContainsKey(string key) => configurations.ContainsKey(key); - - public bool TryGetValue(string key, out FieldConfiguration value) => configurations.TryGetValue(key, out value!); - -#pragma warning disable CS8766 // Nullability of reference types in return type doesn't match implicitly implemented member (possibly because of nullability attributes). - public FieldConfiguration? this[string fieldName] => configurations.TryGetValue(fieldName, out FieldConfiguration? value) ? value : null; -#pragma warning restore CS8766 // Nullability of reference types in return type doesn't match implicitly implemented member (possibly because of nullability attributes). - - public IEnumerable Keys => configurations.Keys; - public IEnumerable Values => configurations.Values; - public IReadOnlyDictionary? Attributes => attributesConfiguration; - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - - public IReadOnlyDictionary? ValidationRules => validationRules; - public IEnumerator> GetEnumerator() => configurations.GetEnumerator(); - public int Count => configurations.Count; - - public CultureInfo CultureInfo { get; } = culture; - - public DateTimeKind DateTimeKind { get; } = dateTimeKind; -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvReadingOptions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvReadingOptions.cs deleted file mode 100644 index 5ed3f9f6..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/CsvReadingOptions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.RegularExpressions; -using FluentResults; - -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public class CsvReadingOptions(IDictionary options) -{ - public static readonly CsvReadingOptions Empty = new(new Dictionary()); - - protected readonly IDictionary Options = options; - - public IReadOnlyDictionary Snapshot => Options.AsReadOnly(); - - public Result GetFileOptionsFor(string name) - { - var key = Options.Keys.FirstOrDefault(k => k.IsMatch(name)); - var value = key is null ? null : Options[key]; - - if (value is null) - { - return Result.Fail(new UnexpectedFileNameGivenError(name)); - } - - return new SpecificCsvFileReadingOptions(name, value); - } - - private class UnexpectedFileNameGivenError(string name) : IError - { - public string Message => "Reading configuration for this file is not defined!"; - - public Dictionary Metadata { get; } = new() - { - { nameof(name), name } - }; - - public List Reasons { get; } = new(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/FieldConfiguration.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/FieldConfiguration.cs deleted file mode 100644 index ece8aaf0..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/FieldConfiguration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public record FieldConfiguration(string Pattern); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IBudgetSpecificSettingsRepository.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IBudgetSpecificSettingsRepository.cs deleted file mode 100644 index 2bb6f101..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IBudgetSpecificSettingsRepository.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public interface IBudgetSpecificSettingsRepository -{ - Task GetReadingOptionsFor(TrackedBudget budget, CancellationToken ct); - Task UpdateReadingOptionsFor(TrackedBudget budget, CsvReadingOptions options, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IOutputOptionsUpdater.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IOutputOptionsUpdater.cs deleted file mode 100644 index bbad8c37..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/IOutputOptionsUpdater.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public interface IOutputOptionsChanger -{ - void SetOutputStreamName(string output); - void SetErrorStreamName(string error); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/LogbookWritingOptions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/LogbookWritingOptions.cs deleted file mode 100644 index f2eedff7..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/LogbookWritingOptions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public record LogbookWritingOptions( - string Path, - bool WriteCounts, - bool WriteOperations, - IEnumerable Ranges); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/NamedRange.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/NamedRange.cs deleted file mode 100644 index 1151071f..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/NamedRange.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public record NamedRange(string Name, DateTime From, DateTime Till); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/OutputOptions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/OutputOptions.cs deleted file mode 100644 index 5746a5f8..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/OutputOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public class OutputOptions -{ - public bool ShowSuccesses { get; set; } - - public string OutputStreamName { get; set; } = string.Empty; - - public string ErrorStreamName { get; set; } = string.Empty; -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/SpecificCsvFileReadingOptions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/SpecificCsvFileReadingOptions.cs deleted file mode 100644 index 77b9ecab..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Options/SpecificCsvFileReadingOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -public class SpecificCsvFileReadingOptions( - string fileName, - CsvFileReadingOptions options -) : CsvFileReadingOptions(options.ToDictionary(), options.CultureInfo, options.DateTimeKind, options.Attributes, options.ValidationRules) -{ - public string FileName { get; } = fileName; -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/ILogbookWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/ILogbookWriter.cs deleted file mode 100644 index ea31c3b8..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/ILogbookWriter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -public interface ILogbookWriter -{ - public Task Write(CriteriaBasedLogbook? logbook, LogbookWritingOptions options, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IObjectWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IObjectWriter.cs deleted file mode 100644 index 0c2da36a..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IObjectWriter.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -public interface IObjectWriter -{ - Task Write(T criterion, CancellationToken ct); - Task Write(T criterion, string streamName, CancellationToken ct); - Task Write(IEnumerable collection, CancellationToken ct); - Task Write(IEnumerable collection, string streamName, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IOutputStreamProvider.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IOutputStreamProvider.cs deleted file mode 100644 index 8ee2a049..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IOutputStreamProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -public interface IOutputStreamProvider -{ - Task GetOutput(string name = ""); - Task GetError(string name = ""); - Task ReleaseStreamsAsync(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IResultWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IResultWriter.cs deleted file mode 100644 index 437aa2cf..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Contracts/Output/IResultWriter.cs +++ /dev/null @@ -1,8 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -public interface IResultWriter where T : IResultBase -{ - Task Write(T result, CancellationToken ct); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvFileParserShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvFileParserShould.cs deleted file mode 100644 index f2608aba..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvFileParserShould.cs +++ /dev/null @@ -1,82 +0,0 @@ -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; -using NVs.Budget.Infrastructure.IO.Console.Tests.TestData.FileWithDotsInNumbersAndCyrillicAttributes; -using NVs.Budget.Infrastructure.IO.Console.Tests.TestData.ValidFile; -using NVs.Budget.Utilities.Testing; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class CsvFileParserShould(TestBed testBed) : IClassFixture -{ - private readonly Fixture _fixture = new() { Customizations = { new ReadableExpressionsBuilder() }}; - - [Fact(Skip = "Does not work on CI runners properly")] - public async Task ParseValidFile() - { - testBed.AccountsRepository = new FakeReadOnlyBudgetsRepository([]); - var parser = testBed.GetCsvParser(); - var options = await testBed.GetOptionsFrom("TestData/ValidFile/validFileConfig.yml"); - var stream = File.OpenRead("TestData/ValidFile/validFile.csv"); - - var name = "validFile.csv"; - var operations = await parser.ReadUnregisteredOperations(new StreamReader(stream), options.GetFileOptionsFor(name).Value, CancellationToken.None).ToListAsync(); - - operations.Should().AllSatisfy(r => r.Should().BeSuccess($"{r.PrintoutReasons()}")); - operations.Select(o => o.Value).Should().BeEquivalentTo(ValidFile.Operations); - } - - [Fact] - public async Task ParseFileWithDotsInNumbersAndCyrillicComments() - { - testBed.AccountsRepository = new FakeReadOnlyBudgetsRepository([]); - var parser = testBed.GetCsvParser(); - var options = await testBed.GetOptionsFrom("TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.yml"); - var stream = File.OpenRead("TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.csv"); - - var name = "file.csv"; - var operations = await parser.ReadUnregisteredOperations(new StreamReader(stream), options.GetFileOptionsFor(name).Value, CancellationToken.None).ToListAsync(); - operations.Should().AllSatisfy(r => r.Should().BeSuccess($"{r.PrintoutReasons()}")); - operations.Select(o => o.Value).Should().BeEquivalentTo(FileWithDotsInNumbersAndCyrillicAttributes.Operations); - } - - [Fact] - public async Task ParseTrackedTransactionsFileSuccessfully() - { - _fixture.Inject(LogbookCriteria.Universal); - var budgets = _fixture.Create>().Take(2).ToArray(); - var operations = new List(); - - foreach (var account in budgets) - { - using (_fixture.SetAccount(account)) - { - operations.AddRange(_fixture.Create>().Take(10)); - } - } - - testBed.AccountsRepository = new FakeReadOnlyBudgetsRepository(budgets); - - await using var streams = new FakeStreamsProvider(); - testBed.StreamProvider = streams; - - var writer = testBed.GetServiceProvider().GetRequiredService>(); - - await writer.Write(operations, CancellationToken.None); - var data = streams.GetOutputBytes(); - - await using var stream = new MemoryStream(data); - using var reader = new StreamReader(stream); - var parser = testBed.GetCsvParser(); - - var actual = await parser.ReadTrackedOperation(reader, CancellationToken.None).ToListAsync(); - actual.Should().AllSatisfy(r => r.Should().BeSuccess()); - actual.Count.Should().Be(operations.Count); - actual.Select(r => r.Value).Should().BeEquivalentTo(operations); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvReaderShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvReaderShould.cs deleted file mode 100644 index 1a95d89f..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/CsvReaderShould.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text; -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using Microsoft.Extensions.Options; -using Moq; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output.Budgets; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class CsvReaderShould -{ - private readonly Fixture _fixture = new(); - private readonly FakeStreamsProvider _streams = new(); - private readonly YamlBasedCsvReadingOptionsReader _reader; - private readonly YamlBasedCsvReadingOptionsWriter _writer; - - public CsvReaderShould() - { - var opts = new Mock>(); - opts.Setup(o => o.Value).Returns(new OutputOptions()); - - _reader = new YamlBasedCsvReadingOptionsReader(); - _writer = new YamlBasedCsvReadingOptionsWriter(_streams, opts.Object); - } - - [Fact] - public async Task BeAbleToReadFromWriter() - { - var expected = _fixture.Create(); - - await _writer.Write(expected, CancellationToken.None); - var outputBytes = _streams.GetOutputBytes(); - var text = Encoding.Default.GetString(outputBytes); - _streams.ResetInput(outputBytes); - - var input = await _streams.GetInput(); - var result = await _reader.ReadFrom(input.Value, CancellationToken.None); - - result.Should().BeSuccess(); - result.Value.Should().BeEquivalentTo(expected); - - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeReadOnlyBudgetsRepository.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeReadOnlyBudgetsRepository.cs deleted file mode 100644 index 13b65f5a..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeReadOnlyBudgetsRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Linq.Expressions; -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; - -internal class FakeReadOnlyBudgetsRepository(TrackedBudget[] budgets) : IBudgetsRepository -{ - public Task> Get(Expression> filter, CancellationToken ct) - { - return Task.FromResult((IReadOnlyCollection)budgets.Where(filter.Compile()).ToList()); - } - - public Task> Register(UnregisteredBudget newBudget, Owner owner, CancellationToken ct) - { - Assert.Fail("This method should not be invoked!"); - throw new NotImplementedException(); - } - - public Task> Update(TrackedBudget budget, CancellationToken ct) - { - Assert.Fail("This method should not be invoked!"); - throw new NotImplementedException(); - } - - public Task Remove(TrackedBudget budget, CancellationToken ct) - { - Assert.Fail("This method should not be invoked!"); - throw new NotImplementedException(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeStreamsProvider.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeStreamsProvider.cs deleted file mode 100644 index 8f8ca460..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/Mocks/FakeStreamsProvider.cs +++ /dev/null @@ -1,50 +0,0 @@ -using FluentResults; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Output; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; - -public class FakeStreamsProvider : IInputStreamProvider, IOutputStreamProvider, IDisposable, IAsyncDisposable -{ - private MemoryStream _iStream = new(); - private readonly MemoryStream _oStream = new(); - - public Task> GetInput(string name = "") - { - return Task.FromResult(Result.Ok(new StreamReader(_iStream))); - } - - public void ResetInput(byte[] data) - { - _iStream = new MemoryStream(data); - } - - public Task GetOutput(string name = "") => Task.FromResult(new StreamWriter(_oStream)); - - - public byte[] GetOutputBytes() => _oStream.ToArray(); - - public Task GetError(string name = "") - { - Assert.Fail("This method should not be invoked!"); - throw new NotImplementedException(); - } - - public void Dispose() - { - _iStream.Dispose(); - _oStream.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _iStream.DisposeAsync(); - await _oStream.DisposeAsync(); - } - - public async Task ReleaseStreamsAsync() - { - await _iStream.FlushAsync(); - await _oStream.FlushAsync(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/NVs.Budget.Infrastructure.IO.Console.Tests.csproj b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/NVs.Budget.Infrastructure.IO.Console.Tests.csproj deleted file mode 100644 index 4989e447..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/NVs.Budget.Infrastructure.IO.Console.Tests.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestBed.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestBed.cs deleted file mode 100644 index 5311db67..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestBed.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class TestBed -{ - public IServiceProvider GetServiceProvider() - { - var configuration = new ConfigurationBuilder().Build(); - - var collection = new ServiceCollection().AddConsoleIO().UseConsoleIO(configuration); - if (AccountsRepository is not null) - { - collection.AddSingleton(AccountsRepository); - } - - if (StreamProvider is not null) - { - collection.AddSingleton(StreamProvider); - } - - return collection.BuildServiceProvider(); - } - - internal IBudgetsRepository? AccountsRepository { get; set; } - - internal IOutputStreamProvider? StreamProvider { get; set; } = new FakeStreamsProvider(); - - internal IOperationsReader GetCsvParser() => GetServiceProvider().GetRequiredService(); - - internal async Task GetOptionsFrom(string file) - { - using var stream = new StreamReader(File.OpenRead(file)); - var reader = GetServiceProvider().GetRequiredService(); - - var result = await reader.ReadFrom(stream, CancellationToken.None); - return result.Value; - } - - -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/FileWithDotsInNumbersAndCyrillicAttributes.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/FileWithDotsInNumbersAndCyrillicAttributes.cs deleted file mode 100644 index e150350a..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/FileWithDotsInNumbersAndCyrillicAttributes.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests.TestData.FileWithDotsInNumbersAndCyrillicAttributes; - -internal class FileWithDotsInNumbersAndCyrillicAttributes -{ - public static readonly List Operations = new() - { - new UnregisteredOperation(DateTime.Parse("2023-10-07 16:10:19+00"), new Money(-5000, CurrencyIsoCode.RUB), "Детский Мир", - new Dictionary - { - { "Category", "Детские товары" } - }) - }; -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.csv b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.csv deleted file mode 100644 index 7139b5b5..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.csv +++ /dev/null @@ -1,2 +0,0 @@ -"Timestamp","Amount","Currency","Description","Budget","Bank","Category" -"2023-10-07 16:10:19+00","-5000.00","RUB","Детский Мир","Бюджет","Банк","Детские товары" diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.yml b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.yml deleted file mode 100644 index 79820a7d..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/FileWithDotsInNumbersAndCyrillicAttributes/file.yml +++ /dev/null @@ -1,14 +0,0 @@ -CsvReadingOptions: - file.csv: - CultureCode: en-US - Timestamp: '{0}' - Amount: '{1}' - CurrencyCode: '{2}' - Description: '{3}' - Attributes: - Category: '{6}' - ValidationRules: - not a header: - FieldConfiguration: '{0}' - Condition: NotEquals - Value: Timestamp diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/ValidFile.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/ValidFile.cs deleted file mode 100644 index a77265b0..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/ValidFile.cs +++ /dev/null @@ -1,29 +0,0 @@ -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests.TestData.ValidFile; - -internal class ValidFile -{ - public static readonly List Operations = new() - { - new UnregisteredOperation(DateTime.Parse("30.10.2023"), new Money(-11, CurrencyIsoCode.RUB), "MOSKVA\\OZON RU", - new Dictionary - { - { "MCC", "5300" }, - { "Category", "Супермаркеты" } - }), - new UnregisteredOperation(DateTime.Parse("23.10.2023"), new Money(32, CurrencyIsoCode.RUB), "Зарплатка", - new Dictionary - { - { "MCC", "" }, - { "Category", "Зарплата" } - }), - new UnregisteredOperation(DateTime.Parse("22.10.2023"), new Money(-1515.2m , CurrencyIsoCode.RUB), "Moscow\\YANDEX 5814 EDA", - new Dictionary - { - { "MCC", "3990" }, - { "Category", "Фастфуд" } - }) - }; -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFile.csv b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFile.csv deleted file mode 100644 index 7294129c..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFile.csv +++ /dev/null @@ -1,4 +0,0 @@ -Дата операции;Дата проводки;Название счета;Номер счета;Название карты;Номер карты;Описание операции;Сумма;Валюта;Статус;Категория;MCC код;Тип;;Комментарий -"30,10,2023";"31,10,2023";Счет;4.0817810204830155e+019;Карта;123;MOSKVA\OZON RU;-11;RUB;Выполнен;Супермаркеты;5300;;; -"23,10,2023";"23,10,2023";Еще счет;4.0817810404830036e+019;;;Зарплатка;32;RUB;;Зарплата;;Пополнение;; -"22,10,2023";"24,10,2023";Счет;4.0817810204830155e+019;Карта;123;Moscow\YANDEX 5814 EDA;-1515,2;RUB;Выполнен;Фастфуд;3990;;; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFileConfig.yml b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFileConfig.yml deleted file mode 100644 index 0089cf68..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/TestData/ValidFile/validFileConfig.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -CsvReadingOptions: - validFile.csv: - Timestamp: "{0}" - Amount: "{7}" - CurrencyCode: "{8}" - Description: "{6}" - Attributes: - MCC: "{11}" - Category: "{10}" - ValidationRules: - not a header: - FieldConfiguration: "{0}" - Condition: NotEquals - Value: Дата операции -CultureCode: ru-RU diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTaggingRuleReaderShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTaggingRuleReaderShould.cs deleted file mode 100644 index 8341a612..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTaggingRuleReaderShould.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Text; -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using Microsoft.Extensions.Options; -using Moq; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; -using NVs.Budget.Utilities.Expressions; -using NVs.Budget.Utilities.Testing; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class YamlBasedTaggingRuleReaderShould -{ - private readonly FakeStreamsProvider _streams = new(); - private readonly Fixture _fixture = new() { Customizations = { new ReadableExpressionsBuilder() }}; - private readonly YamlBasedTaggingCriteriaReader _reader = new(ReadableExpressionsParser.Default); - private readonly YamlBasedTaggingCriteriaWriter _writer; - - public YamlBasedTaggingRuleReaderShould() - { - var options = new Mock>(); - options.SetupGet(o => o.Value).Returns(new OutputOptions()); - - _writer = new YamlBasedTaggingCriteriaWriter(_streams, options.Object); - } - - - [Fact] - public async Task ReadValues() - { - var tags = new Dictionary>() - { - { @"o => ""Expences""", ["o => o.Amount.IsNegative()", "o => o.Amount.Amount < 0"] }, - { @"o => ""Incomes""", ["o => o.Amount.IsPositive()", "o => o.Amount.Amount > 0"] }, - { "o => o.Timestamp.Month.ToString() + o.Timestamp.Year.ToString()", ["o => true"] } - }; - - var expected = tags.SelectMany(kv => kv.Value.Select(v => new TaggingCriterion( - ReadableExpressionsParser.Default.ParseUnaryConversion(kv.Key).Value, - ReadableExpressionsParser.Default.ParseUnaryPredicate(v).Value - ))).ToList(); - var stream = GetStream(tags); - - var actual = await _reader.ReadFrom(new StreamReader(stream), CancellationToken.None).ToListAsync(); - - actual.Should().AllSatisfy(r => r.Should().BeSuccess()); - actual.Select(r => r.Value).Should().BeEquivalentTo(expected); - } - - private MemoryStream GetStream(Dictionary> dict) - { - var builder = new StringBuilder(); - foreach (var (key, value) in dict) - { - builder.AppendLine($"{key}:"); - foreach (var val in value) - { - builder.AppendLine($" - {val}"); - } - } - - var text = builder.ToString(); - - return new MemoryStream(Encoding.UTF8.GetBytes(text)); - } - - [Fact] - public async Task ReadValuesWrittenByWriter() - { - var criteria = _fixture.Create>().Take(4).ToList(); - await _writer.Write(criteria, CancellationToken.None); - - var data = _streams.GetOutputBytes(); - - using var streamReader = new StreamReader(new MemoryStream(data)); - - var actual = await _reader.ReadFrom(streamReader, CancellationToken.None).ToListAsync(); - actual.Should().AllSatisfy(r => r.Should().BeSuccess()); - actual.Select(r => r.Value).Should().BeEquivalentTo(criteria); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTransferCriteriaReaderShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTransferCriteriaReaderShould.cs deleted file mode 100644 index 3c865c0c..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlBasedTransferCriteriaReaderShould.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Text; -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using Microsoft.Extensions.Options; -using Moq; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; -using NVs.Budget.Utilities.Expressions; -using NVs.Budget.Utilities.Testing; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class YamlBasedTransferCriteriaWriterShould -{ - private readonly Fixture _fixture = new() { Customizations = { new ReadableExpressionsBuilder() } }; - private readonly FakeStreamsProvider _streams = new(); - private readonly YamlBasedTransferCriteriaWriter _writer; - private readonly YamlBasedTransferCriteriaReader _reader; - - public YamlBasedTransferCriteriaWriterShould() - { - var options = new Mock>(); - options.SetupGet(x => x.Value).Returns(new OutputOptions()); - - _writer = new YamlBasedTransferCriteriaWriter(_streams, options.Object); - _reader = new YamlBasedTransferCriteriaReader(new()); - } - - [Fact] - public async Task ReadRulesWrittenByWriter() - { - var criteria = _fixture.Create>().Take(7).ToList(); - await _writer.Write(criteria, CancellationToken.None); - - var data = _streams.GetOutputBytes(); - - using var streamReader = new StreamReader(new MemoryStream(data)); - - var result = await _reader.ReadFrom(streamReader, CancellationToken.None).ToListAsync(); - result.Should().AllSatisfy(r => r.Should().BeSuccess()); - result.Select(r => r.Value).Should().BeEquivalentTo(criteria); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlLogbookCriteriaReaderShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlLogbookCriteriaReaderShould.cs deleted file mode 100644 index 79b79e65..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console.Tests/YamlLogbookCriteriaReaderShould.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Text; -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using Microsoft.Extensions.Options; -using Moq; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Domain.ValueObjects.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Tests.Mocks; -using NVs.Budget.Utilities.Expressions; -using NVs.Budget.Utilities.Testing; - -namespace NVs.Budget.Infrastructure.IO.Console.Tests; - -public class YamlLogbookRulesetReaderShould -{ - private readonly Fixture _fixture = new() { Customizations = { new ReadableExpressionsBuilder() } }; - private readonly FakeStreamsProvider _streams = new(); - private readonly YamlBasedLogbookCriteriaReader _reader = new(ReadableExpressionsParser.Default); - private readonly YamlBasedLogbookCriteriaWriter _writer; - - public YamlLogbookRulesetReaderShould() - { - var opts = new Mock>(); - opts.Setup(o => o.Value).Returns(new OutputOptions()); - _writer = new YamlBasedLogbookCriteriaWriter(_streams, opts.Object); - } - - [Fact] - public async Task ParseValidYamlConfig() - { - var yaml = @" -odds: - tags: [ odd ] - subcriteria: - incomes: - predicate: o=> o.Amount.Amount > 0 - else: -evens: - tags: - - odd - - excluded - type: excluding - subcriteria: - subst: - substitution: o => ""Year"" + o.Timestamp.Year.ToString() -"; - var bytes = Encoding.UTF8.GetBytes(yaml); - - var expected = new UniversalCriterion(string.Empty, [ - new TagBasedCriterion("odds", [new("odd")], TagBasedCriterionType.Including, [ - new PredicateBasedCriterion("incomes", o => o.Amount.Amount > 0, []), - new UniversalCriterion("else") - ]), - new TagBasedCriterion("evens", [new("odd"), new("excluded")], TagBasedCriterionType.Excluding,[ - new SubstitutionBasedCriterion("subst", o => $"Year {o.Timestamp.Year}") - ]) - ]); - - using var streamReader = new StreamReader(new MemoryStream(bytes)); - var result = await _reader.ReadFrom(streamReader, CancellationToken.None); - result.Should().BeSuccess(); - result.Value.GetCriterion().Should().BeEquivalentTo(expected); - } - - [Fact] - public async Task ReadCriteriaWrittenByWriter() - { - var logbook = new LogbookCriteria(string.Empty, - [ - new("odds", [ - new LogbookCriteria("incomes", null, null, null, null, - ReadableExpressionsParser.Default.ParseUnaryPredicate("o=> o.Amount.Amount > 0").Value, null), - new LogbookCriteria("else", null, null, null, null, null, true) - ], - TagBasedCriterionType.Including, [new("odd")], - null, null, null), - new LogbookCriteria("evens", - [ - new LogbookCriteria("subst", null, null, null, - ReadableExpressionsParser.Default.ParseUnaryConversion("o => \"Year\" + o.Timestamp.Year.ToString()").Value, - null, null) - ], - TagBasedCriterionType.Excluding, [new("odd"), new("excluded")], null, null, null) - ], - null, null, null, null, true); - - await _writer.Write(logbook, CancellationToken.None); - - var data = _streams.GetOutputBytes(); - var text = Encoding.UTF8.GetString(data); - - using var streamReader = new StreamReader(new MemoryStream(data)); - - var result = await _reader.ReadFrom(streamReader, CancellationToken.None); - result.Should().BeSuccess(); - result.Value.Should().BeEquivalentTo(logbook); - } - -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/ConsoleIOExtensions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/ConsoleIOExtensions.cs deleted file mode 100644 index c7143c83..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/ConsoleIOExtensions.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Globalization; -using System.Runtime.CompilerServices; -using CsvHelper.Configuration; -using FluentResults; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Infrastructure.IO.Console.Input; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; -using NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader; -using NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output; -using NVs.Budget.Infrastructure.IO.Console.Output.Budgets; -using NVs.Budget.Infrastructure.IO.Console.Output.Logbook; -using NVs.Budget.Infrastructure.IO.Console.Output.Operations; -using NVs.Budget.Infrastructure.IO.Console.Output.Owners; -using NVs.Budget.Infrastructure.IO.Console.Output.Results; -using NVs.Budget.Utilities.Expressions; - -[assembly:InternalsVisibleTo("NVs.Budget.Infrastructure.IO.Console.Tests")] -namespace NVs.Budget.Infrastructure.IO.Console; - -public static class ConsoleIOExtensions -{ - public static IServiceCollection AddConsoleIO(this IServiceCollection services) - { - services.AddAutoMapper(c => c.AddProfile(new CsvMappingProfile())); - services.AddTransient, OwnersWriter>(); - services.AddTransient, TrackedOperationsWriter>(); - services.AddTransient, TransfersWriter>(); - services.AddTransient, OperationsWriter>(); - services.AddTransient, TrackedBudgetWriter>(); - services.AddTransient, YamlBasedCsvReadingOptionsWriter>(); - services.AddTransient, YamlBasedTransferCriteriaWriter>(); - services.AddTransient, YamlBasedTaggingCriteriaWriter>(); - services.AddTransient, YamlBasedLogbookCriteriaWriter>(); - - services.AddTransient(typeof(IResultWriter<>), typeof(GenericResultWriter<>)); - services.AddTransient>, OwnerResultWriter>(); - services.AddTransient(); - services.AddTransient(); - - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddSingleton(); - - return services; - } - - public static IServiceCollection UseConsoleIO(this IServiceCollection services, IConfiguration configuration) - { - var cultureCode = configuration.GetValue("CultureCode"); - var culture = cultureCode is null ? CultureInfo.CurrentCulture : CultureInfo.GetCultureInfo(cultureCode); - - - services.Configure(configuration.GetSection(nameof(OutputOptions)).Bind); - - services.AddSingleton(configuration); - - - services.AddSingleton(new CsvConfiguration(culture) - { - IgnoreBlankLines = true, - HeaderValidated = null, - HasHeaderRecord = true, - DetectDelimiter = true - }); - - var logbookCriteriaReader = new YamlBasedLogbookCriteriaReader(ReadableExpressionsParser.Default); - services.AddSingleton(logbookCriteriaReader); - - return services; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/AttributesConverter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/AttributesConverter.cs deleted file mode 100644 index 0cfded31..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/AttributesConverter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using AutoMapper; -using NVs.Budget.Utilities.Json; - -namespace NVs.Budget.Infrastructure.IO.Console.Converters; - -internal class AttributesConverter : IValueConverter, string>, IValueConverter> -{ - public static readonly AttributesConverter Instance = new(); - - public string Convert(IDictionary sourceMember, ResolutionContext? _) => sourceMember.ToJsonString(); - public IReadOnlyDictionary Convert(string sourceMember, ResolutionContext? _) => sourceMember.ToDictionary().AsReadOnly(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/MoneyConverter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/MoneyConverter.cs deleted file mode 100644 index ed812def..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/MoneyConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; -using NMoneys; - -namespace NVs.Budget.Infrastructure.IO.Console.Converters; - -internal class MoneyConverter : IValueConverter, IValueConverter -{ - public static readonly MoneyConverter Instance = new(); - public string Convert(Money sourceMember, ResolutionContext? _) => sourceMember.AsQuantity(); - public Money Convert(string sourceMember, ResolutionContext? _) => Money.Parse(sourceMember); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/OwnersConverter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/OwnersConverter.cs deleted file mode 100644 index 5a4c1391..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/OwnersConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; -using NVs.Budget.Domain.Entities.Accounts; - -namespace NVs.Budget.Infrastructure.IO.Console.Converters; - -internal class OwnersConverter : IValueConverter, string> -{ - public static readonly OwnersConverter Instance = new(); - - public string Convert(IReadOnlyCollection owners, ResolutionContext context) => string.Join(',', owners.Select(o => o.Name)); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/TagsConverter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/TagsConverter.cs deleted file mode 100644 index 4e9b0321..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Converters/TagsConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; -using NVs.Budget.Domain.ValueObjects; - -namespace NVs.Budget.Infrastructure.IO.Console.Converters; - -internal class TagsConverter : IValueConverter, string>, IValueConverter> -{ - public static readonly TagsConverter Instance = new(); - public string Convert(IReadOnlyCollection sourceMember, ResolutionContext? _) => string.Join(",", sourceMember.Select(s => s.Value)); - public IReadOnlyCollection Convert(string sourceMember, ResolutionContext? _) => sourceMember.Split(',').Select(s => new Tag(s.Trim())).ToArray(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/ConsoleInputStream.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/ConsoleInputStream.cs deleted file mode 100644 index e6fddc12..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/ConsoleInputStream.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Concurrent; -using FluentResults; -using NVs.Budget.Controllers.Console.Contracts.Errors; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -internal class ConsoleInputStream : IInputStreamProvider, IDisposable -{ - private readonly ConcurrentDictionary _readers = new(); - private volatile bool _disposed; - - public ConsoleInputStream() - { - System.Console.InputEncoding = System.Text.Encoding.GetEncoding(65001); - } - - public Task> GetInput(string name = "") - { - if (_disposed) throw new ObjectDisposedException(nameof(ConsoleInputStream)); - - try - { - var reader = _readers.GetOrAdd(name, CreateReader); - return Task.FromResult(Result.Ok(reader)); - } - catch (Exception e) - { - return Task.FromResult(Result.Fail(new ExceptionBasedError(e))); - } - } - - private StreamReader CreateReader(string name) - { - var stream = string.IsNullOrEmpty(name) - ? System.Console.OpenStandardInput() - : File.OpenRead(name); - - return new StreamReader(stream); - } - - public async Task ReleaseStreamsAsync() - { - var keys = _readers.Keys.ToArray(); - foreach (var key in keys) - { - if (_readers.TryRemove(key, out var reader)) - { - if (reader.BaseStream is FileStream fs) - { - await fs.DisposeAsync(); - } - - reader.Dispose(); - } - } - } - - public void Dispose() - { - _disposed = true; - - foreach (var (_, reader) in _readers) - { - reader.Dispose(); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/CriteriaParser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/CriteriaParser.cs deleted file mode 100644 index d2c5586c..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/CriteriaParser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq.Expressions; -using FluentResults; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Utilities.Expressions; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.Criteria; - -internal class CriteriaParser : ExpressionParser, ICriteriaParser -{ - public Result>> TryParsePredicate(string expression, string paramName) - { - if (string.IsNullOrWhiteSpace(expression)) - { - return (Expression>)(_ => true); - } - try - { - return ParsePredicate(expression, paramName); - } - catch (Exception e) - { - return Result.Fail(new ExceptionBasedError(e)); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/Logbook/YamlBasedLogbookCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/Logbook/YamlBasedLogbookCriteriaReader.cs deleted file mode 100644 index 812dd07b..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/Criteria/Logbook/YamlBasedLogbookCriteriaReader.cs +++ /dev/null @@ -1,274 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Domain.Extensions; -using NVs.Budget.Domain.ValueObjects.Criteria; -using NVs.Budget.Utilities.Expressions; -using YamlDotNet.RepresentationModel; -using Tag = NVs.Budget.Domain.ValueObjects.Tag; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; - -internal class YamlBasedLogbookCriteriaReader(ReadableExpressionsParser parser) : ILogbookCriteriaReader -{ - private static readonly string[] EmptyPath = []; - private static readonly YamlScalarNode CriteriaKey = new("criteria"); - private static readonly YamlScalarNode TagsKey = new("tags"); - private static readonly YamlScalarNode TagsModeKey = new("type"); - private static readonly YamlScalarNode SubcriteriaKey = new("subcriteria"); - private static readonly YamlScalarNode SubstitutionKey = new("substitution"); - private static readonly YamlScalarNode UniversalKey = new("universal"); - - public Task> ReadFrom(StreamReader reader, CancellationToken ct) - { - var stream = new YamlStream(); - try { stream.Load(reader); } catch(Exception e) { return Task.FromResult(Result.Fail(new ExceptionBasedError(e))); } - - var count = stream.Documents.Count; - if (count != 1) - { - return Task.FromResult>( - Result.Fail(new YamlParsingError(count == 0 ? "No YAML document found in input" : "Multiple documents found in input", EmptyPath)) - ); - } - - var document = stream.Documents.Single(); - - var result = ParseCriterion(string.Empty, document.RootNode, EmptyPath); - return Task.FromResult(result); - } - - private IEnumerable> ParseSubcriteria(YamlNode node, ICollection path) - { - if (node is not YamlMappingNode mapping) - { - yield return Result.Fail(new UnexpectedNodeTypeError(node.GetType(), typeof(YamlMappingNode), path)); - yield break; - } - - foreach (var (key, value) in mapping.Children) - { - if (key is not YamlScalarNode scalarKey) - { - yield return Result.Fail(new UnexpectedNodeTypeError(key.GetType(), typeof(YamlScalarNode), path)); - continue; - } - - var description = scalarKey.Value; - yield return ParseCriterion(description, value, path); - } - } - - private Result ParseCriterion(string? description, YamlNode value, ICollection path) - { - if (description is null) - { - return Result.Fail(new YamlParsingError("No description given for criterion", path)); - } - - var nodePath = path.Append(description).ToList(); - - if (value is YamlScalarNode scalar && string.IsNullOrEmpty(scalar.Value)) - { - return new LogbookCriteria(description, null, null, null, null, null, null); - } - - if (value is not YamlMappingNode mapping) - { - return Result.Fail(new UnexpectedNodeTypeError(value.GetType(), typeof(YamlMappingNode), nodePath)); - } - - var subcriteria = Enumerable.Empty>(); - if (mapping.Children.TryGetValue(SubcriteriaKey, out var subcriteriaNode)) - { - subcriteria = ParseSubcriteria(subcriteriaNode, nodePath); - } - - - - var errors = new List(); - var validCriteria = new List(); - foreach (var res in subcriteria) - { - if (res.IsSuccess) - { - validCriteria.Add(res.Value); - } - else - { - errors.AddRange(res.Errors); - } - } - - if (validCriteria.Count == 0) - { - validCriteria = null; - } - - Result result; - if (mapping.Children.ContainsKey(CriteriaKey)) - { - result = ParsePredicate(description, mapping, validCriteria, path); - } - else if (mapping.Children.ContainsKey(TagsKey)) - { - result = ParseTags(description, mapping, validCriteria, path); - } - else if (mapping.Children.ContainsKey(SubstitutionKey)) - { - result = ParseSubstitution(description, mapping, validCriteria, path); - } - else - { - if (mapping.Children.TryGetValue(UniversalKey, out var isUniversalNode) && isUniversalNode is YamlScalarNode isUniversal) - { - if (bool.TryParse(isUniversal.Value, out var isUniversalValue)) - { - result = Result.Ok(new LogbookCriteria(description, validCriteria, null, null, null, null, isUniversalValue)); - } - else - { - result = Result.Fail(new YamlParsingError("Cannot parse boolean value", path.Append(description).Append(UniversalKey.ToString())).WithMetadata("Value", isUniversal.Value)); - } - } - else - { - result = Result.Ok(new LogbookCriteria(description, validCriteria, null,null, null, null, false)); - } - } - - result.WithErrors(errors); - return result; - } - - private Result ParseSubstitution(string description, YamlMappingNode mapping, List? validCriteria, ICollection path) - { - if (validCriteria?.Any() ?? false) - { - return Result.Fail(new YamlParsingError("Substitution node cannot have subcriteria", path.Append(description))); - } - - var substitutionNodePath = path.Append(description).Append(SubstitutionKey.Value!).ToList(); - - if (mapping.Children[SubstitutionKey] is not YamlScalarNode scalarNode) - { - return Result.Fail(new UnexpectedNodeTypeError(mapping.Children[SubstitutionKey].GetType(), typeof(YamlScalarNode), substitutionNodePath)); - } - - var substitution = parser.ParseUnaryConversion(scalarNode.Value!); - if (!substitution.IsSuccess) - { - return substitution.ToResult(); - } - - return new LogbookCriteria(description, null, null, null, substitution.Value, null, null); - } - - private Result ParseTags(string description, YamlMappingNode mapping, List? validCriteria, ICollection path) - { - var tags = ParseTagsList(mapping.Children[TagsKey], path.Append(description)); - var validTags = new List(); - var errors = new List(); - - foreach (var tag in tags) - { - if (tag.IsSuccess) - { - validTags.Add(tag.Value); - } - else - { - errors.AddRange(tag.Errors); - } - } - - var type = TagBasedCriterionType.Including; - if (mapping.Children.TryGetValue(TagsModeKey, out var modeNode)) - { - var modePath = path.Append(description).Append(TagsModeKey.Value!).ToList(); - if (modeNode is not YamlScalarNode modeScalar) - { - return Result.Fail(new UnexpectedNodeTypeError(modeNode.GetType(), typeof(YamlScalarNode), modePath)); - } - - if (!Enum.TryParse(modeScalar.Value, true, out var parsed)) - { - return Result.Fail(new YamlParsingError("Unexpected tagging criterion node value given", modePath).WithMetadata("Value", modeScalar.Value!)); - } - - type = parsed; - } - - var criterion = new LogbookCriteria(description, validCriteria, type, validTags, null, null, null); - return Result.Ok(criterion).WithErrors(errors); - } - - private IEnumerable> ParseTagsList(YamlNode node, IEnumerable path) - { - var tagsPath = path.Append(TagsKey.Value!).ToList(); - if (node is not YamlSequenceNode seq) - { - yield return Result.Fail(new UnexpectedNodeTypeError(node.GetType(), typeof(YamlSequenceNode), tagsPath)); - yield break; - } - - foreach (var tagNode in seq.Children) - { - if (tagNode is not YamlScalarNode tagScalar) - { - yield return Result.Fail(new UnexpectedNodeTypeError(tagNode.GetType(), typeof(YamlScalarNode), tagsPath)); - continue; - } - - if (string.IsNullOrEmpty(tagScalar.Value)) - { - yield return Result.Fail(new YamlParsingError("Empty tag value given", tagsPath)); - continue; - } - - yield return new Tag(tagScalar.Value); - } - } - - private Result ParsePredicate(string description, YamlMappingNode mapping, List? subcriteria, ICollection path) - { - var predicateNode = mapping.Children[CriteriaKey]; - var predicateNodePath = path.Append(description).Append(CriteriaKey.Value!).ToList(); - if (predicateNode is not YamlScalarNode scalarNode) - { - return Result.Fail(new UnexpectedNodeTypeError(predicateNode.GetType(), typeof(YamlScalarNode), predicateNodePath)); - } - - var value = scalarNode.Value; - if (string.IsNullOrEmpty(value)) - { - return Result.Fail(new YamlParsingError("No predicate value given", predicateNodePath)); - } - var parseResult = parser.ParseUnaryPredicate(value); - if (parseResult.IsFailed) - { - return Result.Fail(new YamlParsingError("Failed to parse predicate value", predicateNodePath)).WithReasons(parseResult.Errors); - } - - return new LogbookCriteria(description, subcriteria, null, null, null, parseResult.Value, null); - } -} - - -internal class YamlParsingError(string reason, IEnumerable path) : IError -{ - public string Message { get; } = reason; - public Dictionary Metadata { get; } = new() { {"Path", string.Join('.', path) } }; - public List Reasons { get; } = new(); -} - -internal class UnexpectedNodeTypeError : YamlParsingError -{ - public UnexpectedNodeTypeError(Type type, Type expected, ICollection path) : base("Unexpected node type found", path) - { - Metadata.Add("Key", path.LastOrDefault() ?? string.Empty); - Metadata.Add("Expected", expected.Name); - Metadata.Add("Type", type.Name); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/CsvOperationsReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/CsvOperationsReader.cs deleted file mode 100644 index 8451e0f6..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/CsvOperationsReader.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Runtime.CompilerServices; -using CsvHelper; -using CsvHelper.Configuration; -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader; - -internal class CsvOperationsReader(CsvConfiguration configuration, IBudgetsRepository budgetsRepository) : IOperationsReader -{ - public async IAsyncEnumerable> ReadUnregisteredOperations(StreamReader input, SpecificCsvFileReadingOptions fileOptions, [EnumeratorCancellation] CancellationToken ct) - { - var parser = new CsvParser(input, configuration, true); - var rowParser = new UntrackedRowParser(parser, fileOptions, ct); - - while (await rowParser.ReadAsync()) - { - var validationResult = rowParser.IsRowValid(); - if (validationResult.IsFailed) - { - yield return validationResult.ToResult(); - } - - if (validationResult.Value) - { - yield return rowParser.GetRow(); - } - } - } - - public async IAsyncEnumerable> ReadTrackedOperation(StreamReader input, [EnumeratorCancellation]CancellationToken ct) - { - var parser = new CsvReader(input, configuration, true); - parser.Context.RegisterClassMap(); - - var rowParser = new TrackedRowParser(parser, budgetsRepository, ct); - - while (await rowParser.ReadAsync()) - { - yield return await rowParser.GetRecord(); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/AccountDoesNotExistsError.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/AccountDoesNotExistsError.cs deleted file mode 100644 index f075c3f4..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/Errors/AccountDoesNotExistsError.cs +++ /dev/null @@ -1,10 +0,0 @@ -using FluentResults; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader.Errors; - -internal class BudgetDoesNotExistError(Guid Id) : IError -{ - public string Message => "Budget with such Id does not exist!"; - public Dictionary Metadata { get; } = new() { { nameof(Id), Id } }; - public List Reasons { get; } = new(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/TrackedRowParser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/TrackedRowParser.cs deleted file mode 100644 index 94919ea9..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/TrackedRowParser.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Collections.Concurrent; -using CsvHelper; -using FluentResults; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Domain.ValueObjects; -using NVs.Budget.Infrastructure.IO.Console.Converters; -using NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader.Errors; -using NVs.Budget.Infrastructure.IO.Console.Input.Errors; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader; - -internal class TrackedRowParser(IReader parser, IBudgetsRepository budgetsRepository, CancellationToken cancellationToken) : RowParser(parser, cancellationToken) -{ - private readonly ConcurrentDictionary> _budgets = new(); - - protected override async Task> Convert(CsvTrackedOperation row) - { - var budgetTask = _budgets.GetOrAdd(row.BudgetId, GetBudget); - var budget = await budgetTask; - if (budget is null) - { - return Result.Fail(new BudgetDoesNotExistError(row.BudgetId)); - } - - if (string.IsNullOrEmpty(row.Amount)) - { - return Result.Fail(new RowNotParsedError(Row, [new AttributeParsingError(nameof(row.Amount))])); - } - - Money amount; - try - { - amount = MoneyConverter.Instance.Convert(row.Amount, null); - } - catch (Exception e) - { - return Result.Fail(new RowNotParsedError(Row, [new AttributeParsingError(nameof(row.Amount)), new ExceptionBasedError(e)])); - } - - IReadOnlyCollection tags; - try - { - tags = TagsConverter.Instance.Convert(row.Tags ?? string.Empty, null); - } - catch (Exception e) - { - return Result.Fail(new RowNotParsedError(Row, [new AttributeParsingError(nameof(row.Tags)), new ExceptionBasedError(e)])); - } - - IReadOnlyDictionary attributes; - try - { - attributes = AttributesConverter.Instance.Convert(row.Attributes ?? string.Empty, null); - } - catch (Exception e) - { - return Result.Fail(new RowNotParsedError(Row, [new AttributeParsingError(nameof(row.Attributes)), new ExceptionBasedError(e)])); - } - - - return new TrackedOperation( - row.Id, - DateTime.SpecifyKind(row.Timestamp, DateTimeKind.Local), - amount, - row.Description ?? string.Empty, - budget, - tags, - attributes - ) - { - Version = row.Version - }; - } - - private async Task GetBudget(Guid id) - { - var budgets = await budgetsRepository.Get(a => a.Id == id, CancellationToken); - return budgets.FirstOrDefault(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/UntrackedRowParser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/UntrackedRowParser.cs deleted file mode 100644 index 65259b13..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvOperationsReader/UntrackedRowParser.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; -using CsvHelper; -using FluentResults; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Domain.Extensions; -using NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader.Errors; -using NVs.Budget.Infrastructure.IO.Console.Input.Errors; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvOperationsReader; - -internal partial class UntrackedRowParser(IParser parser, SpecificCsvFileReadingOptions fileOptions, CancellationToken ct) -{ - private static readonly Regex CellsIndexPattern = GenerateCellIndexPattern(); - private volatile int _row = -1; - public async Task ReadAsync() - { - ct.ThrowIfCancellationRequested(); - - var hasRow = await parser.ReadAsync(); - Interlocked.Increment(ref _row); - - return hasRow; - } - - public Result IsRowValid() - { - if (string.IsNullOrWhiteSpace(parser.RawRecord)) - { - return false; - } - - if (fileOptions.ValidationRules is { Count: > 0 }) - { - foreach (var (key,rule) in fileOptions.ValidationRules) - { - var value = ReadRaw(rule.FieldConfiguration); - if (value.IsFailed) return value.ToResult(); - - switch (rule.Condition) - { - case ValidationRule.ValidationCondition.Equals: - return value.Value.Equals(rule.Value); - - case ValidationRule.ValidationCondition.NotEquals: - return !value.Value.Equals(rule.Value); - - default: - throw new ArgumentOutOfRangeException(nameof(rule.Condition)); - } - } - } - - return true; - } - - public Result GetRow() - { - var timestampResult = ReadField(nameof(UnregisteredOperation.Timestamp), s => DateTime.SpecifyKind(DateTime.Parse(s), fileOptions.DateTimeKind)); - if (timestampResult.IsFailed) - { - return BuildParseError(timestampResult.Errors); - } - - var currencyResult = ReadField(nameof(UnregisteredOperation.Amount.CurrencyCode), Currency.Get); - if (currencyResult.IsFailed) - { - return BuildParseError(currencyResult.Errors); - } - - var moneyResult = ReadField(nameof(UnregisteredOperation.Amount), m => new Money(decimal.Parse(m, fileOptions.CultureInfo), currencyResult.Value)); - if (moneyResult.IsFailed) - { - return BuildParseError(moneyResult.Errors); - } - - var descriptionResult = ReadField(nameof(UnregisteredOperation.Description), s => s); - if (descriptionResult.IsFailed) - { - return BuildParseError(descriptionResult.Errors); - } - - var attributesResult = ReadAttributes(); - if (attributesResult.IsFailed) - { - return BuildParseError(attributesResult.Errors); - } - - return new UnregisteredOperation( - timestampResult.Value, - moneyResult.Value, - descriptionResult.Value, - attributesResult.Value - ); - } - - private Result?> ReadAttributes() - { - var attributes = new Dictionary(); - var attributesOptions = fileOptions.Attributes ?? new Dictionary(); - foreach (var (name, config) in attributesOptions) - { - var value = ReadRaw(config); - if (value.IsFailed) - { - return Result.Fail(new AttributeParsingError(name)); - } - - attributes.Add(name, value.Value); - } - - return attributes.AsReadOnly(); - } - - private Result ReadField(string fieldName, Func convertFn) - { - var fileOption = fileOptions[fieldName]; - if (fileOption is null) - { - return Result.Fail(new NoFieldOptionsProvidedFor(fieldName)); - } - - var rawValue = ReadRaw(fileOption); - if (rawValue.IsFailed) - { - return rawValue.ToResult(); - } - - try - { - return convertFn(rawValue.Value); - } - catch (Exception e) - { - return Result.Fail(new ExceptionBasedError(e).WithMetadata(nameof(fieldName), fieldName)); - } - } - - private Result ReadRaw(FieldConfiguration fileOption) - { - var usedCells = CellsIndexPattern.Matches(fileOption.Pattern).Select(m => (match: m, index: int.Parse(m.Groups[1].Value))).ToList(); - if (usedCells.Count > 0) - { - var values = usedCells.Select(m => m.index).Distinct().Select(i => (index: i, value: parser[i])).ToDictionary(v => v.index, v => v.value); - - var matchIndex = 0; - var strpos = 0; - var builder = new StringBuilder(); - while (matchIndex < usedCells.Count) - { - var match = usedCells[matchIndex]; - if (strpos < match.match.Index) - { - builder.Append(fileOption.Pattern, strpos, match.match.Index - strpos); - strpos = match.match.Index; - } - - builder.Append(values[match.index]); - strpos += match.match.Length; - matchIndex++; - } - - if (strpos < fileOption.Pattern.Length) - { - builder.Append(fileOption.Pattern, strpos, fileOption.Pattern.Length - strpos); - } - - return builder.ToString(); - } - - return fileOption.Pattern; - } - - private Result BuildParseError(List errors) => Result.Fail(new RowNotParsedError(_row, errors).WithMetadata(nameof(fileOptions.FileName), fileOptions.FileName)); - - [GeneratedRegex("{(\\d+)}", RegexOptions.Compiled)] - private static partial Regex GenerateCellIndexPattern(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/CsvTransfersReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/CsvTransfersReader.cs deleted file mode 100644 index efaf5c74..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/CsvTransfersReader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Runtime.CompilerServices; -using CsvHelper; -using CsvHelper.Configuration; -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Output.Operations; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader; - -internal class CsvTransfersReader(CsvConfiguration configuration, IStreamingOperationRepository repository) : ITransfersReader -{ - public async IAsyncEnumerable> ReadUnregisteredTransfers(StreamReader input, [EnumeratorCancellation] CancellationToken ct) - { - var parser = new CsvReader(input, configuration, true); - parser.Context.RegisterClassMap(); - - var reader = new TransferRowReader(parser, ct, repository); - - while (await reader.ReadAsync()) - { - yield return await reader.GetRecord(); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SinkNotFoundError.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SinkNotFoundError.cs deleted file mode 100644 index 95d1cb20..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SinkNotFoundError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader.Errors; - -internal class SinkNotFoundError(Guid sinkId) : IError -{ - public string Message { get; } = "Sink operation not found!"; - public Dictionary Metadata { get; } = new() { { nameof(UnregisteredTransfer.Sink), sinkId } }; - public List Reasons { get; } = new(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SourceNotFoundError.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SourceNotFoundError.cs deleted file mode 100644 index e6652167..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/Errors/SourceNotFoundError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Entities.Budgeting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader.Errors; - -internal class SourceNotFoundError(Guid sourceId) : IError -{ - public string Message { get; } = "Source operation not found!"; - public Dictionary Metadata { get; } = new() { { nameof(UnregisteredTransfer.Source), sourceId } }; - public List Reasons { get; } = new(); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/TransferRowReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/TransferRowReader.cs deleted file mode 100644 index aba8c4ea..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/CsvTransfersReader/TransferRowReader.cs +++ /dev/null @@ -1,44 +0,0 @@ -using CsvHelper; -using FluentResults; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Infrastructure.IO.Console.Converters; -using NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader.Errors; -using NVs.Budget.Infrastructure.IO.Console.Input.Errors; -using NVs.Budget.Infrastructure.IO.Console.Output.Operations; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.IO.Console.Input.CsvTransfersReader; - -internal class TransferRowReader(IReader parser, CancellationToken cancellationToken, IStreamingOperationRepository repository) : RowParser(parser, cancellationToken) -{ - protected override async Task> Convert(CsvTransfer row) - { - Guid[] ids = [row.SourceId, row.SinkId]; - var ops = repository.Get(o => ids.Contains(o.Id), CancellationToken); - var dict = await ops.ToDictionaryAsync(o => o.Id, CancellationToken); - - if (!dict.TryGetValue(row.SourceId, out TrackedOperation? source)) - { - return Result.Fail(new SourceNotFoundError(row.SourceId)); - } - - if (!dict.TryGetValue(row.SinkId, out TrackedOperation? sink)) - { - return Result.Fail(new SinkNotFoundError(row.SinkId)); - } - - Money fee; - try - { - fee = MoneyConverter.Instance.Convert(row.Fee ?? string.Empty, null); - } - catch (Exception e) - { - return Result.Fail(new RowNotParsedError(Row, [new AttributeParsingError(nameof(row.Fee)), new ExceptionBasedError(e)])); - } - - return new UnregisteredTransfer(source, sink, fee, row.Comment ?? string.Empty, row.Accuracy); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/RowParser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/RowParser.cs deleted file mode 100644 index fc37047e..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/RowParser.cs +++ /dev/null @@ -1,49 +0,0 @@ -using CsvHelper; -using FluentResults; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Infrastructure.IO.Console.Input.Errors; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -internal abstract class RowParser(IReader parser, CancellationToken cancellationToken) -{ - private volatile int _row = -1; - - protected readonly CancellationToken CancellationToken = cancellationToken; - - protected int Row => _row; - - public async Task ReadAsync() - { - CancellationToken.ThrowIfCancellationRequested(); - - var result = await parser.ReadAsync(); - if (result) - { - Interlocked.Increment(ref _row); - } - - return result; - } - - public async Task> GetRecord() - { - TRow row; - try - { - row = parser.GetRecord(); - if (row is null) - { - return Result.Fail(new RowNotParsedError(_row, [])); - } - } - catch (Exception e) - { - return Result.Fail(new RowNotParsedError(_row, [new ExceptionBasedError(e)])); - } - - return await Convert(row); - } - - protected abstract Task> Convert(TRow row); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedCsvReadingOptionsReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedCsvReadingOptionsReader.cs deleted file mode 100644 index 24a28d89..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedCsvReadingOptionsReader.cs +++ /dev/null @@ -1,298 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; -using FluentResults; -using NVs.Budget.Domain.Extensions; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; -using NVs.Budget.Infrastructure.IO.Console.Options; -using YamlDotNet.RepresentationModel; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -internal class YamlBasedCsvReadingOptionsReader : YamlReader, ICsvReadingOptionsReader -{ - private static readonly YamlScalarNode RootKey = new("CsvReadingOptions"); - private static readonly YamlScalarNode CultureCodeKey = new("CultureCode"); - private static readonly YamlScalarNode DateTimeKindKey = new("DateTimeKind"); - private static readonly YamlScalarNode AttributesKey = new("Attributes"); - private static readonly YamlScalarNode ValidationRulesKey = new("ValidationRules"); - private static readonly YamlScalarNode FieldConfigurationKey = new("FieldConfiguration"); - private static readonly YamlScalarNode ConditionKey = new("Condition"); - private static readonly YamlScalarNode ValueKey = new("Value"); - - public Task> ReadFrom(StreamReader reader, CancellationToken ct) => Task.FromResult(ReadSync(reader)); - - private Result ReadSync(StreamReader reader) - { - var mapResult = LoadRootNodeFrom(reader); - if (!mapResult.IsSuccess) - { - return mapResult.ToResult(); - } - - var mapping = mapResult.Value; - - if (!mapping.Children.TryGetValue(RootKey, out var filesNode) || filesNode is not YamlMappingNode files) - { - return Result.Fail(new YamlParsingError("CsvReadingOptions node is not found or not a mapping", [RootKey.ToString()])); - } - - var fileConfigs = new Dictionary(); - var errors = new List(); - - foreach (var (key, value) in files) - { - var filePattern = ReadString(key, [RootKey.ToString()]); - if (filePattern.IsFailed) - { - errors.AddRange(filePattern.Errors); - continue; - } - - ICollection path = [RootKey.ToString(), filePattern.Value]; - - if (value is not YamlMappingNode file) - { - errors.Add(new UnexpectedNodeTypeError(value.GetType(), typeof(YamlMappingNode), path)); - continue; - } - - Regex patten; - try - { - patten = new Regex(filePattern.Value); - } - catch (Exception e) - { - errors.Add(new YamlParsingError("Invalid regex given", path).WithMetadata("Exception", e)); - continue; - } - - var fileResult = ReadFile(file, path); - errors.AddRange(fileResult.Errors); - if (fileResult.IsSuccess) - { - fileConfigs.Add(patten,fileResult.Value); - } - } - - return fileConfigs.Any() - ? Result.Ok(new CsvReadingOptions(fileConfigs)).WithReasons(errors) - : Result.Fail(errors); - } - - private Result ReadFile(YamlMappingNode file, ICollection path) - { - CultureInfo? cultureCode = null; - DateTimeKind kind = DateTimeKind.Local; - var errors = new List(); - IDictionary? attributes = null; - IDictionary? validation = null; - var fields = new Dictionary(); - - foreach (var (key, value) in file) - { - if (CultureCodeKey.Equals(key)) - { - var r = ReadString(value, [..path, CultureCodeKey.ToString()]); - if (r.IsSuccess) - { - cultureCode = CultureInfo.GetCultureInfo(r.Value); - } - else - { - errors.AddRange(r.Errors); - } - } - else if (DateTimeKindKey.Equals(key)) - { - var r = ReadString(value, [..path, DateTimeKindKey.ToString()]); - if (r.IsFailed) - { - errors.AddRange(r.Errors); - } - else - { - if (!Enum.TryParse(r.Value, out kind)) - { - errors.Add(new YamlParsingError("Failed to parse DateTimeKind value", [..path, DateTimeKindKey.ToString()])); - } - } - } - else if (AttributesKey.Equals(key)) - { - if (value is not YamlMappingNode attrs) - { - errors.Add(new UnexpectedNodeTypeError(value.GetType(), typeof(YamlMappingNode), [..path, AttributesKey.ToString()])); - } - else - { - var r = ReadAttributes(attrs, [..path, AttributesKey.ToString()]); - errors.AddRange(r.Errors); - if (r.IsSuccess) - { - attributes = r.Value; - } - } - } - else if (ValidationRulesKey.Equals(key)) - { - if (value is not YamlMappingNode attrs) - { - errors.Add(new UnexpectedNodeTypeError(value.GetType(), typeof(YamlMappingNode), [..path, ValidationRulesKey.ToString()])); - } - else - { - var r = ReadValidationRules(attrs, [..path, ValidationRulesKey.ToString()]); - errors.AddRange(r.Errors); - if (r.IsSuccess) - { - validation = r.Value; - } - } - } - else - { - var keyResult = ReadString(key, [..path]); - if (keyResult.IsSuccess) - { - var valResult = ReadString(value, [..path, keyResult.Value]); - if (valResult.IsSuccess) - { - fields.Add(keyResult.Value, new(valResult.Value)); - } - else - { - errors.AddRange(valResult.Errors); - } - } - else - { - errors.AddRange(keyResult.Errors); - } - } - } - - return fields.Any() - ? Result.Ok(new CsvFileReadingOptions(fields, cultureCode ?? CultureInfo.CurrentCulture, kind, attributes?.AsReadOnly(), validation?.AsReadOnly())) - .WithErrors(errors) - : Result.Fail(errors); - } - - private Result?> ReadAttributes(YamlMappingNode attrs, ICollection path) - { - if (!attrs.Any()) - { - return Result.Ok?>(null); - } - - var fields = new Dictionary(); - var errors = new List(); - - foreach (var (key, value) in attrs) - { - var keyResult = ReadString(key, [..path]); - if (keyResult.IsSuccess) - { - var valResult = ReadString(value, [..path, keyResult.Value]); - if (valResult.IsSuccess) - { - fields.Add(keyResult.Value, new(valResult.Value)); - } - else - { - errors.AddRange(valResult.Errors); - } - } - else - { - errors.AddRange(keyResult.Errors); - } - } - - return fields.Any() - ? Result.Ok((Dictionary?)fields).WithErrors(errors) - : Result.Fail(errors); - } - - private Result?> ReadValidationRules(YamlMappingNode attrs, ICollection path) - { - if (!attrs.Any()) - { - return Result.Ok?>(null); - } - - var rules = new Dictionary(); - var errors = new List(); - - foreach (var (key,value) in attrs) - { - var keyResult = ReadString(key, path); - errors.AddRange(keyResult.Errors); - if (keyResult.IsFailed) - { - continue; - } - - var valResult = ReadValidationRule(value, [..path, keyResult.Value]); - errors.AddRange(valResult.Errors); - if (valResult.IsSuccess) - { - rules.Add(keyResult.Value, valResult.Value); - } - } - - return rules.Any() - ? Result.Ok((Dictionary?)rules).WithErrors(errors) - : Result.Fail(errors); - } - - private Result ReadValidationRule(YamlNode node, ICollection path) - { - if (node is not YamlMappingNode mapping) - { - return Result.Fail(new UnexpectedNodeTypeError(node.GetType(), typeof(YamlMappingNode), path)); - } - - if (!mapping.Children.TryGetValue(FieldConfigurationKey, out var fieldConfigNode)) - { - return Result.Fail(new YamlParsingError($"Attribute {FieldConfigurationKey} not found", path)); - } - - if (!mapping.Children.TryGetValue(ConditionKey, out var conditionNode)) - { - return Result.Fail(new YamlParsingError($"Attribute {ConditionKey} not found", path)); - } - - if (!mapping.Children.TryGetValue(ValueKey, out var valueNode)) - { - return Result.Fail(new YamlParsingError($"Attribute {ValueKey} not found", path)); - } - - var fieldConfig = ReadString(fieldConfigNode, [..path, FieldConfigurationKey.ToString()]); - if (fieldConfig.IsFailed) - { - return fieldConfig.ToResult(); - } - - var conditionText = ReadString(conditionNode, [..path, ConditionKey.ToString()]); - if (conditionText.IsFailed) - { - return conditionText.ToResult(); - } - - if (!Enum.TryParse(conditionText.Value, out ValidationRule.ValidationCondition condition)) - { - return Result.Fail( - new YamlParsingError("Unexpected ValidationCondition value given", [..path, ValidationRulesKey.ToString()]).WithMetadata("Value", conditionText) - ); - } - - var value = ReadString(valueNode, [..path, ValueKey.ToString()]); - if (value.IsFailed) - { - return value.ToResult(); - } - - return new ValidationRule(new(fieldConfig.Value), condition, value.Value); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTaggingCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTaggingCriteriaReader.cs deleted file mode 100644 index c6a56770..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTaggingCriteriaReader.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; -using NVs.Budget.Utilities.Expressions; -using YamlDotNet.RepresentationModel; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -internal class YamlBasedTaggingCriteriaReader(ReadableExpressionsParser parser) : YamlReader, ITaggingCriteriaReader -{ - public IAsyncEnumerable> ReadFrom(StreamReader reader, CancellationToken ct) - { - var results = ReadSync(reader); - return results.ToAsyncEnumerable(); - } - - private IEnumerable> ReadSync(StreamReader reader) - { - var rootResult = LoadRootNodeFrom(reader); - if (rootResult.IsFailed) - { - yield return rootResult.ToResult(); - yield break; - } - - var root = rootResult.Value; - foreach (var (tagKey, tagCriteria) in root) - { - var tag = ReadString(tagKey, EmptyPath); - if (!tag.IsSuccess) - { - yield return tag.ToResult(); - continue; - } - - if (tagCriteria is not YamlSequenceNode criteria) - { - yield return Result.Fail(new UnexpectedNodeTypeError(tagCriteria.GetType(), typeof(YamlSequenceNode), [tag.Value])); - continue; - } - - var tagExpr = parser.ParseUnaryConversion(tag.Value); - if (tagExpr.IsFailed) - { - yield return Result.Fail(new YamlParsingError("Failed to parse tagging expression", [tag.Value])).WithErrors(tagExpr.Errors); - continue; - } - - foreach (var criterionNode in criteria) - { - var criterion = ReadString(criterionNode, [tag.Value]); - if (!criterion.IsSuccess) - { - yield return criterion.ToResult(); - } - else - { - var criterionExpr = parser.ParseUnaryPredicate(criterion.Value); - if (criterionExpr.IsFailed) - { - yield return Result.Fail(new YamlParsingError("Failed to parse tagging criterion", [tag.Value])).WithErrors(criterionExpr.Errors); - continue; - } - - - yield return new TaggingCriterion(tagExpr.Value, criterionExpr.Value); - } - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTransferCriteriaReader.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTransferCriteriaReader.cs deleted file mode 100644 index 6feac43a..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlBasedTransferCriteriaReader.cs +++ /dev/null @@ -1,78 +0,0 @@ -using FluentResults; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Extensions; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; -using NVs.Budget.Utilities.Expressions; -using YamlDotNet.RepresentationModel; - -namespace NVs.Budget.Infrastructure.IO.Console.Input; - -internal class YamlBasedTransferCriteriaReader(ReadableExpressionsParser parser) : YamlReader, ITransferCriteriaReader -{ - private static readonly YamlScalarNode AccuracyKey = new("Accuracy"); - private static readonly YamlScalarNode CriterionKey = new("Criterion"); - public IAsyncEnumerable> ReadFrom(StreamReader reader, CancellationToken ct) - { - var results = ReadSync(reader); - return results.ToAsyncEnumerable(); - } - - private IEnumerable> ReadSync(StreamReader reader) - { - var rootNode = LoadRootNodeFrom(reader); - if (!rootNode.IsSuccess) - { - yield return rootNode.ToResult(); - yield break; - } - - foreach (var (key, value) in rootNode.Value) - { - var comment = ReadString(key, EmptyPath); - if (comment.IsFailed) - { - yield return comment.ToResult(); - continue; - } - - if (value is not YamlMappingNode mapping) - { - yield return Result.Fail(new UnexpectedNodeTypeError(value.GetType(), typeof(YamlMappingNode), [comment.Value])); - continue; - } - - DetectionAccuracy accuracy; - var accuracyVal = ReadString(mapping[AccuracyKey], [comment.Value]); - if (accuracyVal.IsFailed) - { - yield return accuracyVal.ToResult(); - continue; - } - - if (!Enum.TryParse(accuracyVal.Value, out accuracy)) - { - yield return Result.Fail( - new YamlParsingError("Unexpected Accuracy value given", [comment.Value, AccuracyKey.ToString()]) - .WithMetadata("Value", accuracyVal.Value) - ); - } - - var criterionVal = ReadString(mapping[CriterionKey], [comment.Value]); - if (criterionVal.IsFailed) - { - yield return criterionVal.ToResult(); - continue; - } - - var expr = parser.ParseBinaryPredicate(criterionVal.Value); - if (expr.IsFailed) - { - yield return Result.Fail(new YamlParsingError("Failed to parse criterion", [comment.Value, CriterionKey.ToString()])).WithReasons(expr.Reasons); - continue; - } - - yield return new TransferCriterion(accuracy, comment.Value, expr.Value); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/MappingProfile.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/MappingProfile.cs deleted file mode 100644 index 35da7798..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/MappingProfile.cs +++ /dev/null @@ -1,36 +0,0 @@ -using AutoMapper; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Infrastructure.IO.Console.Converters; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -namespace NVs.Budget.Infrastructure.IO.Console; - -internal class CsvMappingProfile : Profile -{ - public CsvMappingProfile() - { - CreateMap() - .ForMember(c => c.Amount, o => o.ConvertUsing(MoneyConverter.Instance)) - .ForMember(c => c.Budget, o => o.MapFrom(t => t.Budget.Name)) - .ForMember(c => c.Tags, o => o.ConvertUsing(TagsConverter.Instance, t => t.Tags)) - .ForMember(c => c.Attributes, o => o.ConvertUsing(AttributesConverter.Instance, t => t.Attributes)); - - CreateMap() - .ForMember(c => c.Amount, o => o.ConvertUsing(MoneyConverter.Instance)) - .ForMember(c => c.BudgetId, o => o.MapFrom(t => t.Budget.Id)) - .ForMember(c => c.Budget, o => o.MapFrom(t => t.Budget.Name)) - .ForMember(c => c.Tags, o => o.ConvertUsing(TagsConverter.Instance, t => t.Tags)) - .ForMember(c => c.Attributes, o => o.ConvertUsing(AttributesConverter.Instance, t => t.Attributes)); - - CreateMap() - .ForMember(c => c.SourceId, o => o.MapFrom(t => t.Source.Id)) - .ForMember(c => c.SinkId, o => o.MapFrom(t => t.Sink.Id)) - .ForMember(c => c.Fee, o => o.ConvertUsing(MoneyConverter.Instance)); - - CreateMap() - .ForMember(c => c.Owners, o => o.ConvertUsing(OwnersConverter.Instance)); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudget.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudget.cs deleted file mode 100644 index c53236d9..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudget.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal class CsvBudget -{ - public Guid Id { get; init; } - - public string? Name { get; init; } - - public string? Owners { get; init; } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudgetClassMap.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudgetClassMap.cs deleted file mode 100644 index 7b7e7b3d..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvBudgetClassMap.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CsvHelper.Configuration; - -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal class CsvBudgetClassMap : ClassMap -{ - public CsvBudgetClassMap() - { - Map(a => a.Id); - Map(a => a.Name); - Map(a => a.Owners); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperation.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperation.cs deleted file mode 100644 index 28300244..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperation.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal class CsvOperation -{ - public DateTime Timestamp { get; init; } - public string? Amount { get; init; } - public string? Description { get; init; } - public string? Tags { get; init; } - public string? Attributes { get; init; } - public string? Budget { get; init; } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperationClassMap.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperationClassMap.cs deleted file mode 100644 index 2dab411c..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvOperationClassMap.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CsvHelper.Configuration; - -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal sealed class CsvOperationClassMap : ClassMap -{ - public CsvOperationClassMap() - { - Map(m => m.Timestamp).Index(0); - Map(m => m.Amount).Index(1); - Map(m => m.Description).Index(2); - Map(m => m.Tags).Index(4); - Map(m => m.Attributes).Index(5); - Map(m => m.Budget).Index(6); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperation.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperation.cs deleted file mode 100644 index db638001..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal class CsvTrackedOperation : CsvOperation -{ - public Guid Id { get; init; } - public string? Version { get; init; } - public Guid BudgetId { get; init; } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperationClassMap.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperationClassMap.cs deleted file mode 100644 index a59c4780..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Models/CsvTrackedOperationClassMap.cs +++ /dev/null @@ -1,19 +0,0 @@ -using CsvHelper.Configuration; - -namespace NVs.Budget.Infrastructure.IO.Console.Models; - -internal sealed class CsvTrackedOperationClassMap : ClassMap -{ - public CsvTrackedOperationClassMap() - { - Map(m => m.Id).Index(0); - Map(m => m.Timestamp).Index(1); - Map(m => m.Amount).Index(2); - Map(m => m.Description).Index(3); - Map(m => m.Version).Index(4); - Map(m => m.Tags).Index(5); - Map(m => m.Attributes).Index(6); - Map(m => m.BudgetId).Index(7); - Map(m => m.Budget).Index(8); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/NVs.Budget.Infrastructure.IO.Console.csproj b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/NVs.Budget.Infrastructure.IO.Console.csproj deleted file mode 100644 index d99deb7f..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/NVs.Budget.Infrastructure.IO.Console.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - ..\..\Hosts\NVs.Budget.Hosts.Console\bin\Debug\net8.0\System.Linq.Async.dll - - - - diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Options/OutputOptionsChanger.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Options/OutputOptionsChanger.cs deleted file mode 100644 index 47a80fbc..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Options/OutputOptionsChanger.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Options; - -internal class OutputOptionsChanger(IConfiguration configuration) : IOutputOptionsChanger -{ - public void SetOutputStreamName(string output) - { - configuration.GetSection(nameof(OutputOptions))[nameof(OutputOptions.OutputStreamName)] = output; - } - - public void SetErrorStreamName(string error) - { - configuration.GetSection(nameof(OutputOptions))[nameof(OutputOptions.ErrorStreamName)] = error; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/TrackedBudgetWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/TrackedBudgetWriter.cs deleted file mode 100644 index 13816459..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/TrackedBudgetWriter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections; -using System.Text; -using AutoMapper; -using CsvHelper.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Budgets; - -internal class TrackedBudgetWriter(IOutputStreamProvider streams, IOptionsSnapshot options, IMapper mapper, CsvConfiguration config) - : CsvObjectWriter(streams, options, mapper, config) -{ - public override Task Write(IEnumerable collection, string streamName, CancellationToken ct) => DoWrite(collection, streamName, WriteDetails, ct); - - private async Task WriteDetails(StreamWriter writer, TrackedBudget budget, CancellationToken ct) - { - await writer.WriteLineAsync($"Tagging criteria: {budget.TaggingCriteria.Count}"); - await writer.WriteLineAsync($"Transfer criteria: {budget.TransferCriteria.Count}"); - - var subcriteria = CountSubcriteria(budget.LogbookCriteria).Aggregate("Logbook criteria", (acc, count) => $"{acc}: {count}"); - await writer.WriteLineAsync(subcriteria); - - return false; - } - - private IEnumerable CountSubcriteria(LogbookCriteria criteria) - { - Queue current = new(); - Queue children = new(); - - current.Enqueue(criteria); - - int value = 0; - while (current.Count > 0) - { - var currentCriteria = current.Dequeue(); - if (currentCriteria.Subcriteria != null) - { - value += currentCriteria.Subcriteria.Count; - - foreach (var subcriteria in currentCriteria.Subcriteria) - { - children.Enqueue(subcriteria); - } - } - - if (current.Count == 0) - { - if (value != 0) - { - yield return value; - } - - current = children; - children = new(); - - value = 0; - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/YamlBasedCsvReadingOptionsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/YamlBasedCsvReadingOptionsWriter.cs deleted file mode 100644 index 11ba1648..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Budgets/YamlBasedCsvReadingOptionsWriter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.RegularExpressions; -using Microsoft.Extensions.Options; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Budgets; - -public class YamlBasedCsvReadingOptionsWriter(IOutputStreamProvider streams, IOptionsSnapshot options) : IObjectWriter -{ - private static readonly Regex UnsafeCharsPattern = new("[{} ]", RegexOptions.Compiled); - - public Task Write(CsvReadingOptions criterion, CancellationToken ct) => Write(criterion, options.Value.OutputStreamName, ct); - public async Task Write(CsvReadingOptions criterion, string streamName, CancellationToken ct) - { - var writer = await streams.GetOutput(streamName); - await writer.WriteLineAsync("CsvReadingOptions:"); - foreach (var (pattern, fileOpts) in criterion.Snapshot) - { - await writer.WriteLineAsync($" {Encode(pattern.ToString())}:"); - await writer.WriteLineAsync(" CultureCode: " + fileOpts.CultureInfo.Name); - await writer.WriteLineAsync(" DateTimeKind: " + fileOpts.DateTimeKind); - foreach (var (field, config) in fileOpts) - { - await writer.WriteLineAsync($" {Encode(field)}: {Encode(config.Pattern)}"); - } - - await writer.WriteLineAsync(" Attributes:"); - if (fileOpts.Attributes is not null) - { - foreach (var (name, config) in fileOpts.Attributes) - { - await writer.WriteLineAsync($" {Encode(name)}: {Encode(config.Pattern)}"); - } - } - - await writer.WriteLineAsync(" ValidationRules:"); - if (fileOpts.ValidationRules is not null) - { - foreach (var (name,rule) in fileOpts.ValidationRules) - { - await writer.WriteLineAsync($" {Encode(name)}:"); - await writer.WriteLineAsync($" FieldConfiguration: {Encode(rule.FieldConfiguration.Pattern)}"); - await writer.WriteLineAsync($" Condition: {Encode(rule.Condition.ToString())}"); - await writer.WriteLineAsync($" Value: {Encode(rule.Value)}"); - } - } - } - - await writer.FlushAsync(ct); - } - - private string Encode(string value) - { - if (UnsafeCharsPattern.IsMatch(value)) - { - return $"\"{value.Replace("\"", "\\\"")}\""; - } - - return value; - } - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, options.Value.OutputStreamName, ct); - public async Task Write(IEnumerable collection, string streamName, CancellationToken ct) - { - foreach (var option in collection) - { - await Write(option, streamName, ct); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/ConsoleOutputStreams.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/ConsoleOutputStreams.cs deleted file mode 100644 index 3f5fe8ff..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/ConsoleOutputStreams.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Concurrent; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -internal class ConsoleOutputStreams : IOutputStreamProvider, IAsyncDisposable -{ - private readonly ConcurrentDictionary _outputs = new(); - private readonly ConcurrentDictionary _errors = new(); - private volatile bool _disposed; - - public ConsoleOutputStreams() - { - System.Console.OutputEncoding = System.Text.Encoding.GetEncoding(65001); - } - - public Task GetOutput(string name = "") => Task.FromResult(GetWriter(_outputs, name, CreateOutputWriter)); - - public Task GetError(string name = "") => Task.FromResult(GetWriter(_errors, name, CreateErrorWriter)); - - private StreamWriter GetWriter(ConcurrentDictionary writers, string name, Func createWriter) - { - if (_disposed) throw new ObjectDisposedException(nameof(ConsoleOutputStreams)); - return writers.GetOrAdd(name, createWriter); - } - - private StreamWriter CreateOutputWriter(string arg) - { - var stream = string.IsNullOrEmpty(arg) - ? System.Console.OpenStandardOutput() - : File.OpenWrite(arg); - - return new StreamWriter(stream); - } - - private StreamWriter CreateErrorWriter(string arg) - { - var stream = string.IsNullOrEmpty(arg) - ? System.Console.OpenStandardError() - : File.OpenWrite(arg); - - return new StreamWriter(stream); - } - - public async ValueTask DisposeAsync() - { - _disposed = true; - foreach (var (_, writer) in _outputs.Concat(_errors)) - { - await writer.DisposeAsync(); - } - } - - public async Task ReleaseStreamsAsync() - { - var keys = _outputs.Keys.Concat(_errors.Keys).ToArray(); - foreach (var key in keys) - { - if (_outputs.TryRemove(key, out var writer)) - { - await writer.DisposeAsync(); - } - else if (_errors.TryRemove(key, out writer)) - { - await writer.DisposeAsync(); - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/CsvObjectWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/CsvObjectWriter.cs deleted file mode 100644 index b1b46967..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/CsvObjectWriter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using AutoMapper; -using CsvHelper; -using CsvHelper.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -internal abstract class CsvObjectWriter( - IOutputStreamProvider streams, - IOptionsSnapshot options, - IMapper mapper, - CsvConfiguration config -) : IObjectWriter where TMap : ClassMap -{ - protected IOutputStreamProvider Streams { get; } = streams; - protected IOptionsSnapshot Options { get; } = options; - - public Task Write(T obj, CancellationToken ct) => Write([obj], Options.Value.OutputStreamName, ct); - - public Task Write(T obj, string streamName, CancellationToken ct) => Write([obj], streamName, ct); - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, Options.Value.OutputStreamName, ct); - - public virtual Task Write(IEnumerable collection, string streamName, CancellationToken ct) => DoWrite(collection, streamName, (_, _, _) => Task.FromResult(false), ct); - - protected async Task DoWrite(IEnumerable collection, string streamName, Func> func, CancellationToken ct) - { - var writer = await Streams.GetOutput(streamName); - var csvWriter = new CsvWriter(writer, config, true); - csvWriter.Context.RegisterClassMap(); - - csvWriter.WriteHeader(); - await csvWriter.NextRecordAsync(); - - foreach (var (t, row) in collection.Select(o => (o, mapper.Map(o)))) - { - csvWriter.WriteRecord(row); - await csvWriter.NextRecordAsync(); - - var newLineNeeded = await func(writer, t, ct); - if (newLineNeeded) - { - await writer.WriteLineAsync(); - } - - ct.ThrowIfCancellationRequested(); - } - - await csvWriter.FlushAsync(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/AmountsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/AmountsWriter.cs deleted file mode 100644 index f2f020df..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/AmountsWriter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ClosedXML.Excel; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Logbook; - -internal class AmountsWriter(IXLWorksheet worksheet) : OperationCountsWriter(worksheet) -{ - protected override void SetValue(IXLCell xlCell, Domain.Aggregates.Logbook logbook) - { - var value = logbook.Sum.Amount; - if (value == decimal.Zero) - { - return; - } - - xlCell.SetValue(value); - xlCell.Style.NumberFormat.Format = "# ###0,00\" \"[$\u20bd-419]"; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/CriteriaBasedXLLogbook.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/CriteriaBasedXLLogbook.cs deleted file mode 100644 index f15ddaca..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/CriteriaBasedXLLogbook.cs +++ /dev/null @@ -1,62 +0,0 @@ -using AutoMapper; -using ClosedXML.Excel; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Logbook; - -internal class CriteriaBasedXLLogbook -{ - private readonly CriteriaBasedLogbook _source; - private readonly XLWorkbook _workbook; - - private readonly IEnumerable _ranges; - private readonly OperationCountsWriter? _countSheet; - private readonly AmountsWriter? _amountsSheet; - private readonly LogbookOperationsWriter? _operationsSheet; - - - public CriteriaBasedXLLogbook(CriteriaBasedLogbook logbook, LogbookWritingOptions options, IMapper mapper) - { - _source = logbook; - _ranges = options.Ranges; - _workbook = new XLWorkbook(); - - _amountsSheet = new AmountsWriter(_workbook.AddWorksheet("Amounts")); - - if (options.WriteCounts) - { - _countSheet = new OperationCountsWriter(_workbook.AddWorksheet("Counts")); - } - - if (options.WriteOperations) - { - _operationsSheet = new LogbookOperationsWriter(_workbook.AddWorksheet("Operations"), mapper); - } - } - - public void SaveTo(Stream stream) - { - _countSheet?.ResetPosition(); - _amountsSheet?.ResetPosition(); - _countSheet?.WriteCriteriaNames(_source.Children); - _amountsSheet?.WriteCriteriaNames(_source.Children); - - foreach (var range in _ranges) - { - _countSheet?.WriteRangeName(range); - _amountsSheet?.WriteRangeName(range); - foreach (var (_, logbook) in _source.Children) - { - _countSheet?.WriteValue(logbook, range); - _amountsSheet?.WriteValue(logbook, range); - _operationsSheet?.WriteValue(logbook, range); - } - - _countSheet?.NextCol(); - _amountsSheet?.NextCol(); - } - - _workbook.SaveAs(stream); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookOperationsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookOperationsWriter.cs deleted file mode 100644 index ec0bfc77..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookOperationsWriter.cs +++ /dev/null @@ -1,95 +0,0 @@ -using AutoMapper; -using ClosedXML.Excel; -using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Domain.ValueObjects.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Logbook; - -internal class LogbookOperationsWriter(IXLWorksheet worksheet, IMapper mapper) -{ - private int _rowNum = 1; - private int _colNum = 1; - - public void WriteValue(CriteriaBasedLogbook logbook, NamedRange range) - { - - if (!logbook[range.From, range.Till].Operations.Any()) - { - return; - } - - worksheet.Cell(_rowNum, _colNum).SetValue(logbook.Criterion.Description); - worksheet.Cell(_rowNum, _colNum + 1).SetValue(range.Name); - worksheet.Cell(_rowNum, _colNum + 2).SetValue(range.From); - worksheet.Cell(_rowNum, _colNum + 3).SetValue(range.Till); - worksheet.Cell(_rowNum, _colNum + 4).SetValue(logbook.Criterion.Description); - - var ranged = logbook[range.From, range.Till]; - - var sumCell = worksheet.Cell(_rowNum, _colNum + 4); - sumCell.SetValue(ranged.Sum.Amount); - sumCell.Style.NumberFormat.Format = "# ###0,00\" \"[$\u20bd-419]"; - - _rowNum++; - - - - var logbooks = logbook.Children; - if (logbooks.Any()) - { - _colNum++; - if (logbook.Criterion is SubstitutionBasedCriterion) - { - logbooks = logbooks.OrderBy(c => c.Key.Description).ToDictionary(); - } - - foreach (var (_, child) in logbooks) - { - WriteValue(child, range); - } - - _colNum--; - } - else - { - worksheet.Cell(_rowNum, _colNum).SetValue("|"); - worksheet.Cell(_rowNum, _colNum + 1).SetValue(nameof(CsvTrackedOperation.Id)); - worksheet.Cell(_rowNum, _colNum + 2).SetValue(nameof(CsvTrackedOperation.Timestamp)); - worksheet.Cell(_rowNum, _colNum + 3).SetValue(nameof(CsvTrackedOperation.Amount)); - worksheet.Cell(_rowNum, _colNum + 4).SetValue(nameof(CsvTrackedOperation.Description)); - worksheet.Cell(_rowNum, _colNum + 5).SetValue(nameof(CsvTrackedOperation.Version)); - worksheet.Cell(_rowNum, _colNum + 6).SetValue(nameof(CsvTrackedOperation.Tags)); - worksheet.Cell(_rowNum, _colNum + 7).SetValue(nameof(CsvTrackedOperation.Attributes)); - worksheet.Cell(_rowNum, _colNum + 8).SetValue(nameof(CsvTrackedOperation.BudgetId)); - worksheet.Cell(_rowNum, _colNum + 9).SetValue(nameof(CsvTrackedOperation.Budget)); - _rowNum++; - - foreach (var operation in ranged.Operations.Cast()) - { - var converted = mapper.Map(operation); - - worksheet.Cell(_rowNum, _colNum).SetValue("|"); - worksheet.Cell(_rowNum, _colNum + 1).SetValue(operation.Id.ToString()); - var timestampCell = worksheet.Cell(_rowNum, _colNum + 2); - timestampCell.Style.NumberFormat.Format = "yyyy-MM-dd HH:mm:ss"; - timestampCell.SetValue(operation.Timestamp); - - var amountCell = worksheet.Cell(_rowNum, _colNum + 3); - amountCell.SetValue(operation.Amount.Amount); - amountCell.Style.NumberFormat.Format = "# ###0,00\" \"[$\u20bd-419]"; - worksheet.Cell(_rowNum, _colNum + 4).SetValue(operation.Description); - worksheet.Cell(_rowNum, _colNum + 5).SetValue(operation.Version); - worksheet.Cell(_rowNum, _colNum + 6).SetValue(converted.Tags); - worksheet.Cell(_rowNum, _colNum + 7).SetValue(converted.Attributes); - worksheet.Cell(_rowNum, _colNum + 8).SetValue(converted.BudgetId.ToString()); - worksheet.Cell(_rowNum, _colNum + 9).SetValue(converted.Budget); - - _rowNum++; - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookWriter.cs deleted file mode 100644 index 44af7d75..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/LogbookWriter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using AutoMapper; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Logbook; - -internal class LogbookWriter(IOutputStreamProvider streams, IMapper mapper) : ILogbookWriter -{ - public async Task Write(CriteriaBasedLogbook? logbook, LogbookWritingOptions options, CancellationToken ct) - { - if (logbook is null) - { - return; - } - - if (File.Exists(options.Path)) - { - File.Delete(options.Path); - } - - var streamWriter = await streams.GetOutput(options.Path); - var stream = streamWriter.BaseStream; - - var workbook = new CriteriaBasedXLLogbook(logbook, options, mapper); - workbook.SaveTo(stream); - await stream.FlushAsync(ct); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/OperationCountsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/OperationCountsWriter.cs deleted file mode 100644 index 3495d0a0..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Logbook/OperationCountsWriter.cs +++ /dev/null @@ -1,105 +0,0 @@ -using ClosedXML.Excel; -using NVs.Budget.Domain.Aggregates; -using NVs.Budget.Domain.ValueObjects.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Logbook; - -internal class OperationCountsWriter (IXLWorksheet worksheet) -{ - private readonly Dictionary _criterionPositions = new(); - - private int _criteriaDepth; - private int _rowNum; - private int _colNum; - - public void ResetPosition() - { - _rowNum = 2; _colNum = 1; - } - - public void WriteCriteriaNames(IReadOnlyDictionary children) - { - if (!children.Any()) - { - return; - } - - WriteCriteriaNames(children, 1); - - _colNum = _criteriaDepth + 1; - } - - private void WriteCriteriaNames(IEnumerable> children, int offset, bool orderChildren = false) - { - _criteriaDepth = offset > _criteriaDepth ? offset : _criteriaDepth; - if (orderChildren) - { - children = children.OrderBy(c => c.Key.Description); - } - - foreach (var (criterion, logbook) in children) - { - if (criterion is UniversalCriterion && string.IsNullOrEmpty(criterion.Description) && logbook.IsEmpty) - { - continue; - } - - _colNum = offset; - WriteCriterion(criterion); - WriteCriteriaNames(logbook.Children, offset + 1, criterion is SubstitutionBasedCriterion); - } - } - - private void WriteCriterion(Criterion criterion) - { - var xlCell = worksheet.Cell(_rowNum, _colNum); - xlCell.SetValue(criterion.Description); - - _criterionPositions.Add(criterion, _rowNum); - _rowNum++; - } - - public void WriteRangeName(NamedRange range) - { - var xlCell = worksheet.Cell(1, _colNum); - xlCell.SetValue(range.Name); - xlCell.Style.Alignment.SetTextRotation(90); - } - - public void WriteValue(CriteriaBasedLogbook logbook, NamedRange range) - { - if (logbook is { IsEmpty: true, Criterion: UniversalCriterion } && string.IsNullOrEmpty(logbook.Criterion.Description)) - { - return; - } - - var rowNum = _criterionPositions[logbook.Criterion]; - var xlCell = worksheet.Cell(rowNum, _colNum); - - SetValue(xlCell, logbook[range.From, range.Till]); - - foreach (var (_, child) in logbook.Children) - { - WriteValue(child, range); - } - } - - protected virtual void SetValue(IXLCell xlCell, Domain.Aggregates.Logbook logbook) - { - var value = logbook.Operations.Count(); - if (value == 0) - { - return; - } - - xlCell.SetValue(value); - xlCell.Style.NumberFormat.SetNumberFormatId(1); - } - - public void NextCol() - { - worksheet.Column(_colNum).Width = 12; - _colNum++; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransfer.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransfer.cs deleted file mode 100644 index b5379140..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransfer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Models; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal class CsvTransfer -{ - public Guid SourceId { get; init; } - public Guid SinkId { get; init; } - public string? Fee { get; init; } - public DetectionAccuracy Accuracy { get; init; } - public string? Comment { get; init; } - - public CsvOperation? Source { get; init; } - public CsvOperation? Sink { get; init; } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransferClassMap.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransferClassMap.cs deleted file mode 100644 index 9c02c016..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/CsvTransferClassMap.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CsvHelper.Configuration; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal sealed class CsvTransferClassMap : ClassMap -{ - public CsvTransferClassMap() - { - Map(m => m.SourceId).Index(0); - Map(m => m.SinkId).Index(1); - Map(m => m.Fee).Index(2); - Map(m => m.Accuracy).Index(3); - Map(m => m.Comment).Index(4); - - Map(m => m.Source).Ignore(); - Map(m => m.Sink).Ignore(); - } -} \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/ImportResultWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/ImportResultWriter.cs deleted file mode 100644 index e110943d..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/ImportResultWriter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Application.Contracts.Results; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output.Results; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal class ImportResultWriter( - IOutputStreamProvider streams, - IOptionsSnapshot options, - IObjectWriter opWriter, - IObjectWriter xferWriter) : GenericResultWriter(streams, options) -{ - public override async Task Write(ImportResult result, CancellationToken ct) - { - await base.Write(result, ct); - if (result.IsSuccess) - { - var writer = await OutputStreams.GetOutput(Options.Value.OutputStreamName); - await writer.WriteLineAsync("Operations"); - - foreach (var operation in result.Operations) - { - await opWriter.Write(operation, ct); - } - - await writer.WriteLineAsync(); - await writer.WriteLineAsync("Transfers"); - - foreach (var transfer in result.Transfers) - { - await xferWriter.Write(transfer, ct); - } - - await writer.WriteLineAsync(); - await writer.WriteLineAsync("Duplicates"); - - foreach (var duplicates in result.Duplicates) - { - foreach (var duplicate in duplicates) - { - await opWriter.Write(duplicate, ct); - } - - await writer.WriteLineAsync(); - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/OperationsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/OperationsWriter.cs deleted file mode 100644 index db3bc6b2..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/OperationsWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; -using CsvHelper.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal class OperationsWriter(IOutputStreamProvider streams, IOptionsSnapshot options, IMapper mapper, CsvConfiguration config) - : CsvObjectWriter(streams, options, mapper, config); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TrackedOperationsWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TrackedOperationsWriter.cs deleted file mode 100644 index 4be23957..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TrackedOperationsWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; -using CsvHelper.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal class TrackedOperationsWriter(IOutputStreamProvider streams, IOptionsSnapshot options, IMapper mapper, CsvConfiguration config) - : CsvObjectWriter(streams, options, mapper, config); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TransfersWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TransfersWriter.cs deleted file mode 100644 index d7d2d3ff..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Operations/TransfersWriter.cs +++ /dev/null @@ -1,23 +0,0 @@ -using AutoMapper; -using CsvHelper.Configuration; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Operations; -using NVs.Budget.Infrastructure.IO.Console.Models; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Operations; - -internal class TransfersWriter(IOutputStreamProvider streams, IOptionsSnapshot options, IMapper mapper, CsvConfiguration config, IObjectWriter opWriter) - : CsvObjectWriter(streams, options, mapper, config) -{ - public override Task Write(IEnumerable collection, string streamName, CancellationToken ct) => DoWrite(collection, streamName, WriteOperations, ct); - - private async Task WriteOperations(StreamWriter _, TrackedTransfer transfer, CancellationToken ct) - { - await opWriter.Write(transfer.Source, ct); - await opWriter.Write(transfer.Sink, ct); - - return true; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnerResultWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnerResultWriter.cs deleted file mode 100644 index 33b67371..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnerResultWriter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentResults; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.IO.Console.Output.Results; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Owners; - -internal class OwnerResultWriter(IOutputStreamProvider outputStreams, IOptionsSnapshot options, IObjectWriter writer) : GenericResultWriter>(outputStreams, options) -{ - public override async Task Write(Result response, CancellationToken ct) - { - await base.Write(response, ct); - if (response.IsSuccess) - { - var owner = response.Value; - await writer.Write(owner, ct); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnersWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnersWriter.cs deleted file mode 100644 index bc9862bb..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Owners/OwnersWriter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Owners; - -internal class OwnersWriter(IOutputStreamProvider streams, IOptionsSnapshot options) : IObjectWriter -{ - public Task Write(TrackedOwner criterion, CancellationToken ct) => Write(criterion, options.Value.OutputStreamName, ct); - public Task Write(TrackedOwner criterion, string streamName, CancellationToken ct) => Write([criterion], streamName, ct); - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, options.Value.OutputStreamName, ct); - public async Task Write(IEnumerable collection, string streamName, CancellationToken ct) - { - var writer = await streams.GetOutput(streamName); - foreach (var owner in collection) - { - ct.ThrowIfCancellationRequested(); - await writer.WriteLineAsync($"[{owner.Id}] {owner.Name} (ver {owner.Version})"); - } - - await writer.FlushAsync(ct); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Results/GenericResultWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Results/GenericResultWriter.cs deleted file mode 100644 index 8a6a8497..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/Results/GenericResultWriter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using FluentResults; -using Microsoft.Extensions.Options; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output.Results; - -internal class GenericResultWriter(IOutputStreamProvider streams, IOptionsSnapshot options): IResultWriter where T : IResultBase -{ - protected readonly IOutputStreamProvider OutputStreams = streams; - protected readonly IOptionsSnapshot Options = options; - - public virtual async Task Write(T response, CancellationToken ct) - { - await WriteErrors(response.Errors, ct); - if (Options.Value.ShowSuccesses) - { - await WriteSuccesses(response.Successes, ct); - } - } - - protected async Task WriteSuccesses(List successes, CancellationToken ct) - { - foreach (var success in successes) - { - var writer = await OutputStreams.GetOutput(Options.Value.OutputStreamName); - - await writer.WriteLineAsync($"OK: {success.Message}"); - foreach (var (key, value) in success.Metadata) - { - ct.ThrowIfCancellationRequested(); - await writer.WriteLineAsync($" [{key}]: {value}"); - } - - await writer.FlushAsync(ct); - } - } - - protected async Task WriteErrors(List errors, CancellationToken ct) - { - var writer = await OutputStreams.GetError(Options.Value.ErrorStreamName); - - foreach (var error in errors) - { - await WriterError(writer, string.Empty, error, ct); - } - - await writer.FlushAsync(ct); - } - - private async Task WriterError(StreamWriter writer, string prefix, IError error, CancellationToken ct) - { - ct.ThrowIfCancellationRequested(); - await writer.WriteLineAsync($"{prefix}E: {error.Message}"); - foreach (var (key, value) in error.Metadata) - { - ct.ThrowIfCancellationRequested(); - await writer.WriteLineAsync($"{prefix} [{key}]: {value}"); - } - - foreach (var reason in error.Reasons) - { - await WriterError(writer, prefix + " ", reason, ct); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedLogbookCriteriaWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedLogbookCriteriaWriter.cs deleted file mode 100644 index 0133a98e..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedLogbookCriteriaWriter.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Options; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -internal class YamlBasedLogbookCriteriaWriter(IOutputStreamProvider streams, IOptionsSnapshot options) : IObjectWriter -{ - private readonly ISerializer _serializer = new SerializerBuilder() - .WithNamingConvention(LowerCaseNamingConvention.Instance) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull | DefaultValuesHandling.OmitEmptyCollections) - .WithTypeConverter(new CustomLogbookPartsConverter()) - .Build(); - - public Task Write(LogbookCriteria criterion, CancellationToken ct) => Write(criterion, options.Value.OutputStreamName, ct); - public async Task Write(LogbookCriteria criterion, string streamName, CancellationToken ct) - { - var stream = await streams.GetOutput(streamName); - _serializer.Serialize(stream, criterion); - await stream.FlushAsync(ct); - } - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, options.Value.OutputStreamName, ct); - public async Task Write(IEnumerable collection, string streamName, CancellationToken ct) - { - foreach (var criteria in collection) - { - await Write(criteria, streamName, ct); - } - } - - private class CustomLogbookPartsConverter : IYamlTypeConverter - { - private static readonly Type[] SupportedTypes = - [ - typeof(LogbookCriteria) - ]; - - public bool Accepts(Type type) => SupportedTypes.Contains(type); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - // this is a one-way converter, so Read is not implemented - throw new NotImplementedException(); - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) - { - if (value is LogbookCriteria criteria) - { - emitter.Emit(new MappingStart(null, null, false, MappingStyle.Block)); - if (criteria.Criteria != null) - { - emitter.Emit(new Scalar(null, "criteria")); - emitter.Emit(new Scalar(null, criteria.Criteria.ToString())); - } - - if (criteria.IsUniversal.HasValue) - { - emitter.Emit(new Scalar(null, "universal")); - emitter.Emit(new Scalar(null, criteria.IsUniversal.Value.ToString().ToLower())); - } - - if (criteria.Substitution != null) - { - emitter.Emit(new Scalar(null, "substitution")); - emitter.Emit(new Scalar(null, criteria.Substitution.ToString())); - } - - if (criteria.Tags != null) - { - emitter.Emit(new Scalar(null, "tags")); - emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); - foreach (var tag in criteria.Tags) - { - emitter.Emit(new Scalar(tag.Value)); - } - - emitter.Emit(new SequenceEnd()); - } - - if (criteria.Type != null) - { - emitter.Emit(new Scalar(null, "type")); - emitter.Emit(new Scalar(criteria.Type.ToString()!)); - } - - var subcriteria = criteria.Subcriteria; - if (subcriteria != null && subcriteria.Any()) - { - emitter.Emit(new Scalar(null, "subcriteria")); - emitter.Emit(new MappingStart()); - - foreach (var subcriterion in subcriteria) - { - emitter.Emit(new Scalar(null, subcriterion.Description)); - serializer(subcriterion); - } - - emitter.Emit(new MappingEnd()); - } - - emitter.Emit(new MappingEnd()); - } - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTaggingCriteriaWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTaggingCriteriaWriter.cs deleted file mode 100644 index a4257fdd..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTaggingCriteriaWriter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Options; -using YamlDotNet.Serialization; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -internal class YamlBasedTaggingCriteriaWriter(IOutputStreamProvider streams, IOptionsSnapshot options) : IObjectWriter -{ - private readonly ISerializer _serializer = new SerializerBuilder().Build(); - - public Task Write(TaggingCriterion criterion, CancellationToken ct) => Write(criterion, options.Value.OutputStreamName, ct); - public Task Write(TaggingCriterion criterion, string steamName, CancellationToken ct) => Write(Enumerable.Repeat(criterion, 1), steamName, ct); - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, options.Value.OutputStreamName, ct); - public async Task Write(IEnumerable collection, string streamName, CancellationToken ct) - { - var dict = collection - .GroupBy(t => t.Tag.ToString()) - .ToDictionary( - g => g.Key, - g => g.Select(t => t.Condition.ToString()).ToList() - ); - - var stream = await streams.GetOutput(streamName); - - _serializer.Serialize(stream, dict); - await stream.FlushAsync(ct); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTransferCriteriaWriter.cs b/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTransferCriteriaWriter.cs deleted file mode 100644 index 3506d0f7..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Output/YamlBasedTransferCriteriaWriter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections; -using Microsoft.Extensions.Options; -using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Infrastructure.IO.Console.Options; - -namespace NVs.Budget.Infrastructure.IO.Console.Output; - -internal class YamlBasedTransferCriteriaWriter(IOutputStreamProvider streams, IOptionsSnapshot options) : IObjectWriter -{ - public Task Write(TransferCriterion criterion, CancellationToken ct) => Write(criterion, options.Value.OutputStreamName, ct); - public async Task Write(TransferCriterion criterion, string streamName, CancellationToken ct) - { - var writer = await streams.GetOutput(streamName); - await writer.WriteLineAsync($"{criterion.Comment}:"); - await writer.WriteLineAsync($" Accuracy: {criterion.Accuracy}"); - await writer.WriteLineAsync($" Criterion: {criterion.Criterion}"); - await writer.FlushAsync(ct); - } - - public Task Write(IEnumerable collection, CancellationToken ct) => Write(collection, options.Value.OutputStreamName, ct); - public async Task Write(IEnumerable collection, string streamName, CancellationToken ct) - { - foreach (var criterion in collection) - { - await Write(criterion, streamName, ct); - } - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/ConsoleIdentityExtensions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/ConsoleIdentityExtensions.cs deleted file mode 100644 index dde84624..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/ConsoleIdentityExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Infrastructure.Identity.Contracts; - -namespace NVs.Budget.Infrastructure.Identity.Console; - -public static class ConsoleIdentityExtensions -{ - public static IServiceCollection AddConsoleIdentity(this IServiceCollection services) - { - services.AddTransient(); - return services; - } -} \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/EnvironmentBasedIdentityService.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/EnvironmentBasedIdentityService.cs deleted file mode 100644 index ec4bffd5..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/EnvironmentBasedIdentityService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Infrastructure.Identity.Contracts; -using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; - -namespace NVs.Budget.Infrastructure.Identity.Console; - -internal class EnvironmentBasedIdentityService(IOwnersRepository owners) : IIdentityService -{ - public async Task GetCurrentUser(CancellationToken ct) - { - var uname = Environment.UserName; - var user = new User - { - Id = uname, - }; - - user.Owner = await owners.Get(user, ct); - - return user; - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/User.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/User.cs deleted file mode 100644 index 241469ef..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/User.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Domain.Entities.Accounts; - -namespace NVs.Budget.Infrastructure.Identity.Console; - -internal class User : IUser -{ - public string Id { get; init; } = string.Empty; - public Owner? Owner { get; set; } - public Owner AsOwner() => Owner ?? new Owner(Owner.Invalid.Id, Id); -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContext.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContext.cs new file mode 100644 index 00000000..3a37822e --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; + +internal class UserMappingContext(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("user_mapping"); + modelBuilder.UseOpenIddict(); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContextDesignTimeDbFactory.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContextDesignTimeDbFactory.cs new file mode 100644 index 00000000..cf64eb73 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Mapping/UserMappingContextDesignTimeDbFactory.cs @@ -0,0 +1,5 @@ +using NVs.Budget.Infrastructure.Persistence.EF.Common; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; + +internal class UserMappingContextDesignTimeDbFactory : DesignTimeContextFactory; \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/NVs.Budget.Infrastructure.Identity.Console.csproj b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.csproj similarity index 63% rename from src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/NVs.Budget.Infrastructure.Identity.Console.csproj rename to src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.csproj index 88d751c7..20898d2a 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.Console/NVs.Budget.Infrastructure.Identity.Console.csproj +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.csproj @@ -7,16 +7,20 @@ - - - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Oauth2BasedIdentityService.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Oauth2BasedIdentityService.cs new file mode 100644 index 00000000..65950160 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Oauth2BasedIdentityService.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authentication; +using NVs.Budget.Application.Contracts.Entities; +using NVs.Budget.Infrastructure.Identity.Contracts; +using Microsoft.AspNetCore.Http; +using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; + +internal class Oauth2BasedIdentityService(IHttpContextAccessor accessor, IOwnersRepository ownersRepo) : IIdentityService +{ + public async Task GetCurrentUser(CancellationToken ct) + { + if (accessor.HttpContext is null) + { + throw new InvalidOperationException("HttpContext is null!"); + } + + var result = await accessor.HttpContext.AuthenticateAsync(); + if (result.Succeeded) + { + var webUser = new WebUser(result.Principal); + + var owner = await ownersRepo.Get(webUser, ct); + if (owner is not null) + { + return new WebUser(webUser.Id, owner); + } + var registrationResult = await ownersRepo.Register(new WebUser(result.Principal), ct); + if (registrationResult.IsSuccess) + { + return new WebUser(webUser.Id, registrationResult.Value); + } + + throw new InvalidOperationException("Failed to register user: " + registrationResult.Errors.Aggregate("", (s, error) => s + Environment.NewLine + error.Message)); + } + + throw new InvalidOperationException("The user is not authenticated!"); + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.Designer.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.Designer.cs new file mode 100644 index 00000000..eeaa0552 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.Designer.cs @@ -0,0 +1,44 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + [DbContext(typeof(UserMappingContext))] + [Migration("20250223085318_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("user_mapping") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping.UserToOwnerMapping", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "OwnerId"); + + b.ToTable("Mappings", "user_mapping"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.cs new file mode 100644 index 00000000..48258934 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223085318_Initial.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "user_mapping"); + + migrationBuilder.CreateTable( + name: "Mappings", + schema: "user_mapping", + columns: table => new + { + OwnerId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Mappings", x => new { x.UserId, x.OwnerId }); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Mappings", + schema: "user_mapping"); + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.Designer.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.Designer.cs new file mode 100644 index 00000000..f6229bb8 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.Designer.cs @@ -0,0 +1,288 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + [DbContext(typeof(UserMappingContext))] + [Migration("20250223095143_OpenIddict")] + partial class OpenIddict + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("user_mapping") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping.UserToOwnerMapping", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "OwnerId"); + + b.ToTable("Mappings", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.cs new file mode 100644 index 00000000..b7798e2a --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250223095143_OpenIddict.cs @@ -0,0 +1,183 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + /// + public partial class OpenIddict : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + schema: "user_mapping", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ClientId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "text", nullable: true), + ClientType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + JsonWebKeySet = table.Column(type: "text", nullable: true), + Permissions = table.Column(type: "text", nullable: true), + PostLogoutRedirectUris = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedirectUris = table.Column(type: "text", nullable: true), + Requirements = table.Column(type: "text", nullable: true), + Settings = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + schema: "user_mapping", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "text", nullable: true), + Descriptions = table.Column(type: "text", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + DisplayNames = table.Column(type: "text", nullable: true), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Properties = table.Column(type: "text", nullable: true), + Resources = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + schema: "user_mapping", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + Properties = table.Column(type: "text", nullable: true), + Scopes = table.Column(type: "text", nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~", + column: x => x.ApplicationId, + principalSchema: "user_mapping", + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + schema: "user_mapping", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + ApplicationId = table.Column(type: "text", nullable: true), + AuthorizationId = table.Column(type: "text", nullable: true), + ConcurrencyToken = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "timestamp with time zone", nullable: true), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: true), + Payload = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + RedemptionDate = table.Column(type: "timestamp with time zone", nullable: true), + ReferenceId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + Status = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "character varying(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "character varying(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalSchema: "user_mapping", + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalSchema: "user_mapping", + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + schema: "user_mapping", + table: "OpenIddictApplications", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + schema: "user_mapping", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + schema: "user_mapping", + table: "OpenIddictScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + schema: "user_mapping", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + schema: "user_mapping", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + schema: "user_mapping", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OpenIddictScopes", + schema: "user_mapping"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens", + schema: "user_mapping"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations", + schema: "user_mapping"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications", + schema: "user_mapping"); + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.Designer.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.Designer.cs new file mode 100644 index 00000000..4305ae0b --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + [DbContext(typeof(UserMappingContext))] + [Migration("20250224183713_RemoveMappings")] + partial class RemoveMappings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("user_mapping") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.cs new file mode 100644 index 00000000..72c1dfb8 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/20250224183713_RemoveMappings.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + /// + public partial class RemoveMappings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Mappings", + schema: "user_mapping"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Mappings", + schema: "user_mapping", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + OwnerId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Mappings", x => new { x.UserId, x.OwnerId }); + }); + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/UserMappingContextModelSnapshot.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/UserMappingContextModelSnapshot.cs new file mode 100644 index 00000000..6aa9e5d6 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/Migrations/UserMappingContextModelSnapshot.cs @@ -0,0 +1,272 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence.Migrations +{ + [DbContext(typeof(UserMappingContext))] + partial class UserMappingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("user_mapping") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "user_mapping"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/UserCacheInitializationMiddleware.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/UserCacheInitializationMiddleware.cs new file mode 100644 index 00000000..9317f851 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/Persistence/UserCacheInitializationMiddleware.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using NVs.Budget.Application; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence; + +internal class UserCacheInitializationMiddleware(RequestDelegate next) +{ + public async Task InvokeAsync(HttpContext context, UserCache userCache) + { + var authResult = await context.AuthenticateAsync(); + if (authResult.Succeeded) + { + await userCache.EnsureInitialized(context.RequestAborted); + } + + await next(context); + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebIdentityExtensions.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebIdentityExtensions.cs new file mode 100644 index 00000000..15935324 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebIdentityExtensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NVs.Budget.Application; +using NVs.Budget.Infrastructure.Identity.Contracts; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Mapping; +using NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.Persistence; +using NVs.Budget.Infrastructure.Persistence.EF.Common; +using NVs.Budget.Infrastructure.Persistence.EF.Context; +using OpenIddict.Client.AspNetCore; +using OpenIddict.Client.WebIntegration; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; + +public static class WebIdentityExtensions +{ + // ReSharper disable once InconsistentNaming + private static class URIs + { + public static readonly string YandexRedirectUri = "/auth/callback/login/yandex"; + public static readonly string ChallengeUrl = "/auth/challenge"; + public static readonly string WhoamiUrl = "/auth/whoami"; + public static readonly string LogoutUri = "/auth/logout"; + public static readonly string LoginUrl = "/auth/login"; + } + + public static IServiceCollection AddYandexAuth(this IServiceCollection services, YandexAuthConfig config, string connectionString) + { + services.AddScoped(); + + services.AddDbContext(ops => + { + ops.UseNpgsql(connectionString); + ops.UseOpenIddict(); + }); + + services.AddTransient>(); + + services.AddOpenIddict().AddCore(opts => opts.UseEntityFrameworkCore().UseDbContext()); + + services.AddOpenIddict() + .AddClient(opts => + { + opts.AllowAuthorizationCodeFlow() + .AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); + + opts.UseAspNetCore() + .EnableRedirectionEndpointPassthrough(); + + opts.UseSystemNetHttp(); + + opts.UseWebProviders() + .AddYandex(yopts => + { + yopts.SetClientId(config.ClientId); + yopts.SetClientSecret(config.ClientSecret); + yopts.SetRedirectUri(URIs.YandexRedirectUri); + }); + }); + + services.AddAuthorization(); + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o => + { + o.Cookie.HttpOnly = false; + o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + o.ForwardChallenge = OpenIddictClientAspNetCoreDefaults.AuthenticationScheme; + }); + + services.AddHttpContextAccessor(); + + + return services; + } + + // ReSharper disable once UnusedMethodReturnValue.Global + public static WebApplication UseYandexAuth(this WebApplication app, string authRedirectUri) + { + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseMiddleware(); + + app.MapGet(URIs.ChallengeUrl, () => + { + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = OpenIddictClientWebIntegrationConstants.Providers.Yandex + }); + + return Results.Challenge(properties, authenticationSchemes: [OpenIddictClientAspNetCoreDefaults.AuthenticationScheme]); + }); + + app.MapMethods(URIs.YandexRedirectUri, [HttpMethods.Get, HttpMethods.Post], async (HttpContext context) => + { + var result = await context.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); + if (result.Succeeded) + { + await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, result.Principal); + return Results.Redirect(authRedirectUri); + } + + return Results.BadRequest(result.Failure?.Message); + }); + + app.MapGet(URIs.WhoamiUrl, async (HttpContext context, UserCache cache) => + { + var result = await context.AuthenticateAsync(); + return result.Succeeded + ? Results.Ok(new WhoamiResponse(true, cache.CachedUser, cache.CachedUser.AsOwner())) + : Results.Ok(new WhoamiResponse(false, null, null)); + }); + + app.MapGet(URIs.LogoutUri, async (HttpContext context) => + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Results.Redirect(authRedirectUri); + }); + + app.MapGet(URIs.LoginUrl, (HttpContext _) => Results.Redirect(URIs.ChallengeUrl)); + + return app; + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebUser.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebUser.cs new file mode 100644 index 00000000..8477ad88 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WebUser.cs @@ -0,0 +1,26 @@ +using System.Security.Claims; +using NVs.Budget.Application.Contracts.Entities; +using NVs.Budget.Domain.Entities.Budgets; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; + +internal class WebUser : IUser +{ + private readonly Owner _owner; + public string Id { get; } + + public WebUser(string userId, Owner owner) + { + _owner = owner; + Id = userId; + } + + public WebUser(ClaimsPrincipal principal) + { + Id = principal.FindFirst(ClaimTypes.Email)?.Value ?? throw new ArgumentException("No email claim found", nameof(principal)); + var name = principal.FindFirst(ClaimTypes.Name)?.Value; + _owner = new Owner(Guid.NewGuid(), $"{name} - {Id}"); + } + + public Owner AsOwner() => _owner; +} \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WhoamiResponse.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WhoamiResponse.cs new file mode 100644 index 00000000..0516b6b7 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/WhoamiResponse.cs @@ -0,0 +1,10 @@ +using NVs.Budget.Application.Contracts.Entities; +using NVs.Budget.Domain.Entities.Budgets; + +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; + +internal record WhoamiResponse( + bool IsAuthenticated, + IUser? User, + Owner? Owner +); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/YandexAuthConfig.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/YandexAuthConfig.cs new file mode 100644 index 00000000..91228ec7 --- /dev/null +++ b/src/Infrastructure/NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex/YandexAuthConfig.cs @@ -0,0 +1,3 @@ +namespace NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex; + +public record YandexAuthConfig( string ClientId, string ClientSecret); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetSpecificSettingsRepositoryShould.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetSpecificSettingsRepositoryShould.cs deleted file mode 100644 index 02ffc5c3..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetSpecificSettingsRepositoryShould.cs +++ /dev/null @@ -1,29 +0,0 @@ -using AutoFixture; -using FluentAssertions; -using FluentResults.Extensions.FluentAssertions; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.Persistence.EF.Repositories; -using NVs.Budget.Infrastructure.Persistence.EF.Tests.Fixtures; - -namespace NVs.Budget.Infrastructure.Persistence.EF.Tests; - -public class BudgetSpecificSettingsRepositoryShould(DbContextManager manager): IClassFixture -{ - private readonly BudgetSpecificSettingsRepository _repo = new(manager.GetDbBudgetContext()); - private readonly Fixture _fixture = manager.TestData.Fixture; - private readonly TrackedBudget _budget = manager.TestData.Budgets.First(); - - [Fact] - public async Task StoreAndRetrieveSettings() - { - var expected = _fixture.Create(); - - var result = await _repo.UpdateReadingOptionsFor(_budget, expected, CancellationToken.None); - result.Should().BeSuccess(); - - var actual = await _repo.GetReadingOptionsFor(_budget, CancellationToken.None); - - actual.Should().BeEquivalentTo(expected); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DesignTimeContextFactory.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DesignTimeContextFactory.cs deleted file mode 100644 index de938373..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DesignTimeContextFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; - -namespace NVs.Budget.Infrastructure.Persistence.EF.Context; - -internal sealed class DesignTimeContextFactory : IDesignTimeDbContextFactory -{ - public BudgetContext CreateDbContext(string[] args) - { - var options = new DbContextOptionsBuilder().UseNpgsql().Options; - return new BudgetContext(options); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetSpecificOptionsRepository.cs b/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetSpecificOptionsRepository.cs deleted file mode 100644 index 2a482e86..00000000 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetSpecificOptionsRepository.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; -using FluentResults; -using Microsoft.EntityFrameworkCore; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Infrastructure.IO.Console.Options; -using NVs.Budget.Infrastructure.Persistence.EF.Context; -using NVs.Budget.Infrastructure.Persistence.EF.Entities; -using NVs.Budget.Infrastructure.Persistence.EF.Repositories.Results; - -namespace NVs.Budget.Infrastructure.Persistence.EF.Repositories; - -internal class BudgetSpecificSettingsRepository(BudgetContext context) : IBudgetSpecificSettingsRepository -{ - public async Task GetReadingOptionsFor(TrackedBudget budget, CancellationToken ct) - { - var options = await context.CsvFileReadingOptions - .Include(o => o.FieldConfigurations) - .Include(o => o.AttributesConfiguration) - .Include(o => o.ValidationRules) - .Where(o => o.Budget.Id == budget.Id && o.Deleted == false).ToListAsync(ct); - - return options.Count != 0 - ? new CsvReadingOptions(options.ToDictionary(o => new Regex(o.FileNamePattern), CreateFileReadingOption)) - : CsvReadingOptions.Empty; - } - - private CsvFileReadingOptions CreateFileReadingOption(StoredCsvFileReadingOption option) - { - var info = CultureInfo.GetCultureInfo(option.CultureInfo); - var fieldConfigs = option.FieldConfigurations.ToDictionary(c => c.Field, o => new FieldConfiguration(o.Pattern)); - var attributesConfiguration = option.AttributesConfiguration.ToDictionary(c => c.Field, o => new FieldConfiguration(o.Pattern)); - var validationRules = option.ValidationRules.ToDictionary( - v => v.RuleName, v => new ValidationRule(new(v.FieldConfiguration), v.Condition, v.Value) - ); - - return new CsvFileReadingOptions(fieldConfigs, info, option.DateTimeKind, attributesConfiguration, validationRules); - } - - public async Task UpdateReadingOptionsFor(TrackedBudget budget, CsvReadingOptions options, CancellationToken ct) - { - var storedBudget = await context.Budgets.Include(b => b.CsvReadingOptions).FirstOrDefaultAsync(b => b.Id == budget.Id, ct); - if (storedBudget is null) - { - return Result.Fail(new BudgetDoesNotExistsError(budget)); - } - - storedBudget.CsvReadingOptions.Clear(); - - foreach (var (key, opts) in options.Snapshot) - { - storedBudget.CsvReadingOptions.Add(new StoredCsvFileReadingOption() - { - FileNamePattern = key.ToString(), - DateTimeKind = opts.DateTimeKind, - CultureInfo = opts.CultureInfo.Name, - - FieldConfigurations = opts.Select((kv) => new StoredFieldConfiguration() - { - Field = kv.Key, - Pattern = kv.Value.Pattern - }).ToList(), - - AttributesConfiguration = opts.Attributes?.Select((kv) => new StoredFieldConfiguration() - { - Field = kv.Key, - Pattern = kv.Value.Pattern - }).ToList() ?? [], - - ValidationRules = opts.ValidationRules?.Select(kv => new StoredValidationRule() - { - RuleName = kv.Key, - FieldConfiguration = kv.Value.FieldConfiguration.Pattern, - Condition = kv.Value.Condition, - Value = kv.Value.Value - }).ToList() ?? [] - }); - } - - await context.SaveChangesAsync(ct); - return Result.Ok(); - } -} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DbConnectionInfo.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DbConnectionInfo.cs similarity index 62% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DbConnectionInfo.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DbConnectionInfo.cs index dbd92ac8..dbaf01db 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DbConnectionInfo.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DbConnectionInfo.cs @@ -1,10 +1,10 @@ using Microsoft.EntityFrameworkCore; -namespace NVs.Budget.Infrastructure.Persistence.EF.Context; +namespace NVs.Budget.Infrastructure.Persistence.EF.Common; -internal class DbConnectionInfo : IDbConnectionInfo +public class DbConnectionInfo : IDbConnectionInfo { - public DbConnectionInfo(BudgetContext? context) + public DbConnectionInfo(DbContext? context) { if (context is null) { @@ -15,8 +15,10 @@ public DbConnectionInfo(BudgetContext? context) var connection = context.Database.GetDbConnection(); DataSource = connection.DataSource; Database = connection.Database; + Context = context.GetType().Name; } public string? DataSource { get; } public string? Database { get; } -} \ No newline at end of file + public string? Context { get; } +} diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DesignTimeContextFactory.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DesignTimeContextFactory.cs new file mode 100644 index 00000000..3e39f5fe --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/DesignTimeContextFactory.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace NVs.Budget.Infrastructure.Persistence.EF.Common; + +public class DesignTimeContextFactory : IDesignTimeDbContextFactory where T: DbContext +{ + public T CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder().UseNpgsql().Options; + return (T)(Activator.CreateInstance(typeof(T), options) ?? throw new InvalidOperationException($"Could not create instance of type {typeof(T).Name}")); + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbConnectionInfo.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbConnectionInfo.cs similarity index 54% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbConnectionInfo.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbConnectionInfo.cs index 21e0ba39..3145e0ca 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbConnectionInfo.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbConnectionInfo.cs @@ -1,8 +1,8 @@ -namespace NVs.Budget.Infrastructure.Persistence.EF.Context; +namespace NVs.Budget.Infrastructure.Persistence.EF.Common; public interface IDbConnectionInfo { public string? DataSource { get; } - public string? Database { get; } -} \ No newline at end of file + public string? Context { get; } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbMigrator.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbMigrator.cs similarity index 77% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbMigrator.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbMigrator.cs index 149c19cc..f8922e37 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/IDbMigrator.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/IDbMigrator.cs @@ -1,5 +1,6 @@ namespace NVs.Budget.Infrastructure.Persistence.EF.Context; -public interface IDbMigrator { +public interface IDbMigrator + { Task MigrateAsync(CancellationToken ct); } diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/NVs.Budget.Infrastructure.Persistence.EF.Common.csproj b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/NVs.Budget.Infrastructure.Persistence.EF.Common.csproj new file mode 100644 index 00000000..dd375b6c --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/NVs.Budget.Infrastructure.Persistence.EF.Common.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/PostgreSqlDbMigrator.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/PostgreSqlDbMigrator.cs similarity index 76% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/PostgreSqlDbMigrator.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/PostgreSqlDbMigrator.cs index ee9b088c..28aa2a15 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/PostgreSqlDbMigrator.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Common/PostgreSqlDbMigrator.cs @@ -1,10 +1,11 @@ using System.Data; using Microsoft.EntityFrameworkCore; using Npgsql; +using NVs.Budget.Infrastructure.Persistence.EF.Context; -namespace NVs.Budget.Infrastructure.Persistence.EF.Context; +namespace NVs.Budget.Infrastructure.Persistence.EF.Common; -internal class PostgreSqlDbMigrator(BudgetContext context) : IDbMigrator +public class PostgreSqlDbMigrator(T context) : IDbMigrator where T : DbContext { public async Task MigrateAsync(CancellationToken ct) { diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs similarity index 99% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs index e2a6d64c..db15886f 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/BudgetsRepositoryShould.cs @@ -2,7 +2,7 @@ using FluentAssertions; using FluentResults.Extensions.FluentAssertions; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Domain.ValueObjects.Criteria; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs similarity index 98% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs index 09362204..0dff336d 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/ExchangeRatesRepositoryShould.cs @@ -1,7 +1,7 @@ using AutoFixture; using FluentAssertions; using NMoneys; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.Persistence.EF.Entities; using NVs.Budget.Infrastructure.Persistence.EF.Repositories; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs similarity index 93% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs index 74409dc9..3445576e 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/DatabaseCollectionFixture.cs @@ -1,6 +1,7 @@ using AutoMapper; using AutoMapper.EquivalencyExpression; using Microsoft.EntityFrameworkCore; +using NVs.Budget.Infrastructure.Persistence.EF.Common; using NVs.Budget.Infrastructure.Persistence.EF.Context; using NVs.Budget.Infrastructure.Persistence.EF.Entities; using NVs.Budget.Utilities.Expressions; @@ -27,7 +28,7 @@ public async Task InitializeAsync() { await _postgreSqlContainer.StartAsync(); var context = GetDbBudgetContext(); - await new PostgreSqlDbMigrator(context).MigrateAsync(CancellationToken.None); + await new PostgreSqlDbMigrator(context).MigrateAsync(CancellationToken.None); var owners = Mapper.Map>(TestData.Owners).ToList().ToDictionary(o => o.Id); var budgets = Mapper.Map>(TestData.Budgets).ToList(); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs similarity index 62% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs index fad012dc..377523c0 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TestDataFixture.cs @@ -1,7 +1,7 @@ using AutoFixture; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Utilities.Testing; namespace NVs.Budget.Infrastructure.Persistence.EF.Tests.Fixtures; @@ -17,29 +17,29 @@ public class TestDataFixture public TestDataFixture() { Fixture.Inject(LogbookCriteria.Universal); - var accounts = new List(); + var budgets = new List(); Owners = Fixture.Create>().Take(2).ToList(); foreach (var owner in Owners) { - using (Fixture.SetNamedParameter(nameof(Domain.Entities.Accounts.Budget.Owners).ToLower(), new[] { owner }.AsEnumerable())) + using (Fixture.SetNamedParameter(nameof(Domain.Entities.Budgets.Budget.Owners).ToLower(), new[] { owner }.AsEnumerable())) { - accounts.AddRange(Fixture.Create>().Take(2)); + budgets.AddRange(Fixture.Create>().Take(2)); } } - for (var i = 0; i < accounts.Count; i++) + for (var i = 0; i < budgets.Count; i++) { if (i % 2 == 1) { - foreach (var owner in Owners.Except(accounts[i].Owners)) + foreach (var owner in Owners.Except(budgets[i].Owners)) { - accounts[i].AddOwner(owner); + budgets[i].AddOwner(owner); } } } - Budgets = accounts; + Budgets = budgets; } } diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs similarity index 95% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs index a3efe8f8..20593084 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/Fixtures/TransferOperationsBuilder.cs @@ -2,7 +2,7 @@ using AutoFixture; using AutoFixture.Kernel; using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Utilities.Testing; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/GlobalUsings.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/GlobalUsings.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/GlobalUsings.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/GlobalUsings.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs similarity index 94% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs index d69f82b3..1c53a1ca 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/MappingProfileShould.cs @@ -4,8 +4,8 @@ using FluentAssertions; using NMoneys; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Domain.ValueObjects.Criteria; @@ -36,7 +36,7 @@ public class MappingProfileShould [InlineData(typeof(TaggingCriterion), typeof(StoredTaggingCriterion))] [InlineData(typeof(TransferCriterion), typeof(StoredTransferCriterion))] [InlineData(typeof(LogbookCriteria), typeof(StoredLogbookCriteria))] - [InlineData(typeof(Domain.Entities.Accounts.Budget), typeof(StoredBudget))] + [InlineData(typeof(Domain.Entities.Budgets.Budget), typeof(StoredBudget))] [InlineData(typeof(TrackedBudget), typeof(StoredBudget))] [InlineData(typeof(TrackedOperation), typeof(StoredOperation))] [InlineData(typeof(ExchangeRate), typeof(StoredRate))] diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj similarity index 86% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj index 4a2ba711..8d69bb0a 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj @@ -13,7 +13,6 @@ - @@ -21,14 +20,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs similarity index 94% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs index 022f4905..45628cee 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OperationsRepositoryShould.cs @@ -1,7 +1,7 @@ using AutoFixture; using FluentAssertions; using FluentResults.Extensions.FluentAssertions; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.Persistence.EF.Repositories; using NVs.Budget.Infrastructure.Persistence.EF.Tests.Fixtures; @@ -41,7 +41,7 @@ public async Task RegisterTransactionSuccessfully() trackedTransaction.Should().BeEquivalentTo(transaction); trackedTransaction.Id.Should().NotBe(Guid.Empty); - trackedTransaction.Budget.Should().BeEquivalentTo((Domain.Entities.Accounts.Budget)budget, c => c.ComparingByMembers()); + trackedTransaction.Budget.Should().BeEquivalentTo((Domain.Entities.Budgets.Budget)budget, c => c.ComparingByMembers()); trackedTransaction.Version.Should().NotBeNullOrEmpty(); } @@ -49,11 +49,11 @@ public async Task RegisterTransactionSuccessfully() public async Task UpdateTransactionSuccessfully() { var target = await AddTransaction(); - var newAccount = _testData.Budgets.First(a => a.Id != target.Budget.Id); + var newBudget = _testData.Budgets.First(a => a.Id != target.Budget.Id); TrackedOperation updated; using (_fixture.SetNamedParameter(nameof(target.Id).ToLower(), target.Id)) - using (_fixture.SetNamedParameter(nameof(target.Budget).ToLower(), (Domain.Entities.Accounts.Budget)newAccount)) + using (_fixture.SetNamedParameter(nameof(target.Budget).ToLower(), (Domain.Entities.Budgets.Budget)newBudget)) using (_fixture.SetNamedParameter(nameof(target.Tags).ToLower(), _fixture.Create>().Take(3))) { updated = _fixture.Create(); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs similarity index 97% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs index dea51032..d971882d 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/OwnersRepositoryShould.cs @@ -2,7 +2,7 @@ using FluentAssertions; using FluentResults.Extensions.FluentAssertions; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Infrastructure.Persistence.EF.Repositories; using NVs.Budget.Infrastructure.Persistence.EF.Tests.Fixtures; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs similarity index 87% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs index fd9ff335..27d24585 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF.Tests/TypeReplacerShould.cs @@ -2,8 +2,8 @@ using AutoFixture; using FluentAssertions; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Infrastructure.Persistence.EF.Entities; using NVs.Budget.Utilities.Expressions; using NVs.Budget.Utilities.Testing; @@ -76,8 +76,8 @@ public void ReplaceTypesInMembers() { var id = _fixture.Create(); - Expression> availableAccounts = o => o.Budget.Id == id; - var action = () => availableAccounts.ConvertTypes(MappingProfile.TypeMappings); + Expression> availableBudgets = o => o.Budget.Id == id; + var action = () => availableBudgets.ConvertTypes(MappingProfile.TypeMappings); action.Should().NotThrow(); } @@ -85,8 +85,8 @@ public void ReplaceTypesInMembers() public void ReplaceNestedTypesWithinExtensionMethods() { var owner = _fixture.Create(); - var newAccount = _fixture.Create(); - Expression> expression = a => a.Owners.Any(o => o.Id == owner.Id) && a.Name == newAccount.Name; + var newBudget = _fixture.Create(); + Expression> expression = a => a.Owners.Any(o => o.Id == owner.Id) && a.Name == newBudget.Name; var action = () => expression.ConvertTypes(MappingProfile.TypeMappings); action.Should().NotThrow(); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs similarity index 96% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs index 6d5db7f3..33584ea2 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContext.cs @@ -7,7 +7,7 @@ namespace NVs.Budget.Infrastructure.Persistence.EF.Context; -internal class BudgetContext(DbContextOptions options) : DbContext(options) +internal class BudgetContext(DbContextOptions options) : DbContext(options) { public DbSet Owners { get; init; } = null!; @@ -19,6 +19,7 @@ internal class BudgetContext(DbContextOptions options) : DbContext(options) public DbSet Transfers { get; init; } = null!; + [Obsolete] public DbSet CsvFileReadingOptions { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -30,7 +31,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) var ownBuilder = modelBuilder.Entity(); ownBuilder - .HasMany(o => o.Accounts) + .HasMany(o => o.Budgets) .WithMany(a => a.Owners); ownBuilder.HasIndex(o => o.UserId); diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContextDesignTimeDbFactory.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContextDesignTimeDbFactory.cs new file mode 100644 index 00000000..fd61e14c --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/BudgetContextDesignTimeDbFactory.cs @@ -0,0 +1,5 @@ +using NVs.Budget.Infrastructure.Persistence.EF.Common; + +namespace NVs.Budget.Infrastructure.Persistence.EF.Context; + +internal class BudgetContextDesignTimeDbFactory : DesignTimeContextFactory; \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DictionariesSupport/ShallowDictionaryComparer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/DictionariesSupport/ShallowDictionaryComparer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Context/DictionariesSupport/ShallowDictionaryComparer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Context/DictionariesSupport/ShallowDictionaryComparer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs similarity index 82% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs index dee82757..e2a468c4 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/EfCorePersistenceExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using NVs.Budget.Infrastructure.IO.Console.Options; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; +using NVs.Budget.Infrastructure.Persistence.EF.Common; using NVs.Budget.Infrastructure.Persistence.EF.Context; using NVs.Budget.Infrastructure.Persistence.EF.Entities; using NVs.Budget.Infrastructure.Persistence.EF.Repositories; @@ -19,13 +19,12 @@ public static IServiceCollection AddEfCorePersistence(this IServiceCollection se .AddSingleton(); services.AddTransient() - .AddTransient() .AddTransient() .AddTransient() .AddTransient() .AddTransient() - .AddTransient() - .AddTransient(); + .AddTransient>() + .AddTransient(s => new DbConnectionInfo(s.GetRequiredService())); return services; } diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/DbRecord.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/DbRecord.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/DbRecord.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/DbRecord.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs similarity index 84% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs index 0a3fda77..e9342f44 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/MappingProfile.cs @@ -1,8 +1,8 @@ using AutoMapper; using NMoneys; using NVs.Budget.Application.Contracts.Criteria; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Utilities.Expressions; @@ -33,7 +33,7 @@ public MappingProfile(ReadableExpressionsParser parser) CreateMap().ReverseMap(); CreateMap().ReverseMap(); - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap>, string>().ConstructUsing(r => r.ToString()); @@ -68,7 +68,14 @@ public MappingProfile(ReadableExpressionsParser parser) nameof(TrackedOperation.Timestamp).ToLower(), opt => opt.MapFrom(t => t.Timestamp.ToLocalTime()) ); - CreateMap().ReverseMap(); + CreateMap() + .ForMember(s => s.StartedAt, opt => opt.MapFrom(t => t.StartedAt.ToUniversalTime())) + .ForMember(s => s.CompletedAt, opt => opt.MapFrom(t => t.CompletedAt.ToUniversalTime())); + + CreateMap() + .ForMember(t => t.StartedAt, opt => opt.MapFrom(s => s.StartedAt.ToLocalTime())) + .ForMember(t => t.CompletedAt, opt => opt.MapFrom(s => s.CompletedAt.ToLocalTime())); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredBudget.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredBudget.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredBudget.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredBudget.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredCsvFileReadingOption.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredCsvFileReadingOption.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredCsvFileReadingOption.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredCsvFileReadingOption.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredFieldConfiguration.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredFieldConfiguration.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredFieldConfiguration.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredFieldConfiguration.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredLogbookCriteria.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredLogbookCriteria.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredLogbookCriteria.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredLogbookCriteria.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredMoney.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredMoney.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredMoney.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredMoney.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOperation.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOperation.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOperation.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOperation.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs similarity index 85% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs index 80846791..0b4c1733 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredOwner.cs @@ -14,5 +14,5 @@ internal class StoredOwner(Guid id, string name) : DbRecord, ITrackableEntity Accounts { get; init; } = new List(); + public virtual IList Budgets { get; init; } = new List(); } diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredRate.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredRate.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredRate.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredRate.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTag.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTag.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTag.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTag.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTaggingCriterion.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTaggingCriterion.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTaggingCriterion.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTaggingCriterion.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs similarity index 84% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs index da3a5cda..8cf04fa1 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransfer.cs @@ -9,6 +9,8 @@ internal class StoredTransfer(string comment) : DbRecord public StoredMoney Fee { get; set; } = StoredMoney.Zero; public string Comment { get; set; } = comment; + public DateTime StartedAt { get; set; } + public DateTime CompletedAt { get; set; } public virtual StoredOperation Source { get; set; } = StoredOperation.Invalid; public virtual StoredOperation Sink { get; set; } = StoredOperation.Invalid; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs similarity index 85% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs index ba4e7f51..0c645631 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredTransferCriterion.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.EF.Entities; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs similarity index 89% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs index 69074cf8..6ec07bed 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Entities/StoredValidationRule.cs @@ -1,4 +1,5 @@ -using NVs.Budget.Infrastructure.IO.Console.Options; + +using NVs.Budget.Infrastructure.Files.CSV.Contracts; namespace NVs.Budget.Infrastructure.Persistence.EF.Entities; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20240421103128_Initial.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009071400_removed Bank field.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009072613_AccountId to BugdetId.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009143042_Accounts to Budgets.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241009154459_CsvFileReadingOptions.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241010154052_Tagging Rules.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011083849_Tagging Rule to Tagging Criteria.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241011092428_Transfer Criteria.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241014143608_LogbookCriteria.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241031194700_Transfers_FK.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.Designer.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.Designer.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.Designer.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20241103184804_Adding_IsUniversal_flag.cs diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.Designer.cs new file mode 100644 index 00000000..73f939c5 --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.Designer.cs @@ -0,0 +1,1510 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Persistence.EF.Context; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Persistence.EF.Migrations +{ + [DbContext(typeof(BudgetContext))] + [Migration("20251005130019_RenameAccountToBudget")] + partial class RenameAccountToBudget + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("budget") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "currency_iso_code", new[] { "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", "bhd", "bif", "bmd", "bnd", "bob", "bov", "brl", "bsd", "btn", "bwp", "byn", "byr", "bzd", "cad", "cdf", "che", "chf", "chw", "clf", "clp", "cny", "cop", "cou", "crc", "cuc", "cup", "cve", "czk", "djf", "dkk", "dop", "dzd", "eek", "egp", "ern", "etb", "eur", "fjd", "fkp", "gbp", "gel", "ghs", "gip", "gmd", "gnf", "gtq", "gyd", "hkd", "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "iqd", "irr", "isk", "jmd", "jod", "jpy", "kes", "kgs", "khr", "kmf", "kpw", "krw", "kwd", "kyd", "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "ltl", "lvl", "lyd", "mad", "mdl", "mga", "mkd", "mmk", "mnt", "mop", "mro", "mru", "mur", "mvr", "mwk", "mxn", "mxv", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", "nzd", "omr", "pab", "pen", "pgk", "php", "pkr", "pln", "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", "scr", "sdg", "sek", "sgd", "shp", "sle", "sll", "sos", "srd", "ssp", "std", "stn", "svc", "syp", "szl", "thb", "tjs", "tmt", "tnd", "top", "try", "ttd", "twd", "tzs", "uah", "ugx", "usd", "usn", "uss", "uyi", "uyu", "uyw", "uzs", "ved", "vef", "ves", "vnd", "vuv", "wst", "xaf", "xag", "xau", "xba", "xbb", "xbc", "xbd", "xcd", "xdr", "xof", "xpd", "xpf", "xpt", "xsu", "xts", "xua", "xxx", "yer", "zar", "zmk", "zmw", "zwg", "zwl" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Budgets", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredCsvFileReadingOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CultureInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateTimeKind") + .HasColumnType("integer"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("FileNamePattern") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.ToTable("CsvFileReadingOptions", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Attributes") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.ToTable("Operations", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Owners", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AsOf") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("From") + .HasColumnType("integer"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("To") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Rates", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("SinkId") + .HasColumnType("uuid"); + + b.Property("SourceId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SinkId") + .IsUnique(); + + b.HasIndex("SourceId") + .IsUnique(); + + b.ToTable("Transfers", "budget"); + }); + + modelBuilder.Entity("StoredBudgetStoredOwner", b => + { + b.Property("BudgetsId") + .HasColumnType("uuid"); + + b.Property("OwnersId") + .HasColumnType("uuid"); + + b.HasKey("BudgetsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("StoredBudgetStoredOwner", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTaggingCriterion", "TaggingCriteria", b1 => + { + b1.Property("BudgetId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Condition") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BudgetId", "Id"); + + b1.ToTable("StoredTaggingCriterion", "budget"); + + b1.WithOwner("Budget") + .HasForeignKey("BudgetId"); + + b1.Navigation("Budget"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransferCriterion", "TransferCriteria", b1 => + { + b1.Property("BudgetId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Accuracy") + .HasColumnType("integer"); + + b1.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Criterion") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BudgetId", "Id"); + + b1.ToTable("StoredTransferCriterion", "budget"); + + b1.WithOwner("Budget") + .HasForeignKey("BudgetId"); + + b1.Navigation("Budget"); + }); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "LogbookCriteria", b1 => + { + b1.Property("StoredBudgetId") + .HasColumnType("uuid"); + + b1.Property("Criteria") + .HasColumnType("text"); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("IsUniversal") + .HasColumnType("boolean"); + + b1.Property("Substitution") + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("integer"); + + b1.HasKey("StoredBudgetId"); + + b1.ToTable("Budgets", "budget"); + + b1.ToJson("LogbookCriteria"); + + b1.WithOwner() + .HasForeignKey("StoredBudgetId"); + + b1.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b2 => + { + b2.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("StoredLogbookCriteriaStoredBudgetId", "Id"); + + b2.ToTable("Budgets", "budget"); + + b2.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId"); + }); + + b1.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria.Subcriteria#StoredLogbookCriteria", "Subcriteria", b2 => + { + b2.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Criteria") + .HasColumnType("text"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("IsUniversal") + .HasColumnType("boolean"); + + b2.Property("Substitution") + .HasColumnType("text"); + + b2.Property("Type") + .HasColumnType("integer"); + + b2.HasKey("StoredLogbookCriteriaStoredBudgetId", "Id"); + + b2.ToTable("Budgets", "budget"); + + b2.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId"); + + b2.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b3 => + { + b3.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b3.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b3.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Criteria") + .HasColumnType("text"); + + b3.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b3.Property("IsUniversal") + .HasColumnType("boolean"); + + b3.Property("Substitution") + .HasColumnType("text"); + + b3.Property("Type") + .HasColumnType("integer"); + + b3.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "Id"); + + b3.ToTable("Budgets", "budget"); + + b3.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId"); + + b3.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b4 => + { + b4.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b4.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b4.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b4.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b4.Property("Criteria") + .HasColumnType("text"); + + b4.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b4.Property("IsUniversal") + .HasColumnType("boolean"); + + b4.Property("Substitution") + .HasColumnType("text"); + + b4.Property("Type") + .HasColumnType("integer"); + + b4.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "Id"); + + b4.ToTable("Budgets", "budget"); + + b4.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1"); + + b4.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b5 => + { + b5.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b5.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b5.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b5.Property("Criteria") + .HasColumnType("text"); + + b5.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b5.Property("IsUniversal") + .HasColumnType("boolean"); + + b5.Property("Substitution") + .HasColumnType("text"); + + b5.Property("Type") + .HasColumnType("integer"); + + b5.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "Id"); + + b5.ToTable("Budgets", "budget"); + + b5.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2"); + + b5.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b6 => + { + b6.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b6.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b6.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b6.Property("Criteria") + .HasColumnType("text"); + + b6.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b6.Property("IsUniversal") + .HasColumnType("boolean"); + + b6.Property("Substitution") + .HasColumnType("text"); + + b6.Property("Type") + .HasColumnType("integer"); + + b6.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "Id"); + + b6.ToTable("Budgets", "budget"); + + b6.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3"); + + b6.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b7 => + { + b7.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b7.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b7.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b7.Property("Criteria") + .HasColumnType("text"); + + b7.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b7.Property("IsUniversal") + .HasColumnType("boolean"); + + b7.Property("Substitution") + .HasColumnType("text"); + + b7.Property("Type") + .HasColumnType("integer"); + + b7.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "Id"); + + b7.ToTable("Budgets", "budget"); + + b7.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4"); + + b7.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b8 => + { + b8.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b8.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b8.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b8.Property("Criteria") + .HasColumnType("text"); + + b8.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b8.Property("IsUniversal") + .HasColumnType("boolean"); + + b8.Property("Substitution") + .HasColumnType("text"); + + b8.Property("Type") + .HasColumnType("integer"); + + b8.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "Id"); + + b8.ToTable("Budgets", "budget"); + + b8.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5"); + + b8.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b9 => + { + b9.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b9.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b9.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b9.Property("Criteria") + .HasColumnType("text"); + + b9.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b9.Property("IsUniversal") + .HasColumnType("boolean"); + + b9.Property("Substitution") + .HasColumnType("text"); + + b9.Property("Type") + .HasColumnType("integer"); + + b9.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "Id"); + + b9.ToTable("Budgets", "budget"); + + b9.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6"); + + b9.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b10 => + { + b10.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b10.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b10.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b10.Property("Criteria") + .HasColumnType("text"); + + b10.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b10.Property("IsUniversal") + .HasColumnType("boolean"); + + b10.Property("Substitution") + .HasColumnType("text"); + + b10.Property("Type") + .HasColumnType("integer"); + + b10.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "Id"); + + b10.ToTable("Budgets", "budget"); + + b10.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7"); + + b10.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b11 => + { + b11.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b11.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b11.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b11.Property("Criteria") + .HasColumnType("text"); + + b11.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b11.Property("IsUniversal") + .HasColumnType("boolean"); + + b11.Property("Substitution") + .HasColumnType("text"); + + b11.Property("Type") + .HasColumnType("integer"); + + b11.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "Id"); + + b11.ToTable("Budgets", "budget"); + + b11.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8"); + + b11.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b12 => + { + b12.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b12.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId9") + .HasColumnType("integer"); + + b12.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b12.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b12.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "StoredLogbookCriteriaId9", "Id"); + + b12.ToTable("Budgets", "budget"); + + b12.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "StoredLogbookCriteriaId9"); + }); + + b11.Navigation("Tags"); + }); + + b10.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b11 => + { + b11.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b11.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b11.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b11.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b11.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "Id"); + + b11.ToTable("Budgets", "budget"); + + b11.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8"); + }); + + b10.Navigation("Subcriteria"); + + b10.Navigation("Tags"); + }); + + b9.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b10 => + { + b10.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b10.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b10.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b10.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b10.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "Id"); + + b10.ToTable("Budgets", "budget"); + + b10.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7"); + }); + + b9.Navigation("Subcriteria"); + + b9.Navigation("Tags"); + }); + + b8.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b9 => + { + b9.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b9.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b9.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b9.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b9.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "Id"); + + b9.ToTable("Budgets", "budget"); + + b9.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6"); + }); + + b8.Navigation("Subcriteria"); + + b8.Navigation("Tags"); + }); + + b7.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b8 => + { + b8.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b8.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b8.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b8.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b8.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "Id"); + + b8.ToTable("Budgets", "budget"); + + b8.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5"); + }); + + b7.Navigation("Subcriteria"); + + b7.Navigation("Tags"); + }); + + b6.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b7 => + { + b7.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b7.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b7.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b7.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b7.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "Id"); + + b7.ToTable("Budgets", "budget"); + + b7.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4"); + }); + + b6.Navigation("Subcriteria"); + + b6.Navigation("Tags"); + }); + + b5.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b6 => + { + b6.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b6.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b6.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b6.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b6.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "Id"); + + b6.ToTable("Budgets", "budget"); + + b6.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3"); + }); + + b5.Navigation("Subcriteria"); + + b5.Navigation("Tags"); + }); + + b4.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b5 => + { + b5.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b5.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b5.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b5.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b5.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "Id"); + + b5.ToTable("Budgets", "budget"); + + b5.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2"); + }); + + b4.Navigation("Subcriteria"); + + b4.Navigation("Tags"); + }); + + b3.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b4 => + { + b4.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b4.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b4.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b4.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b4.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b4.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "Id"); + + b4.ToTable("Budgets", "budget"); + + b4.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1"); + }); + + b3.Navigation("Subcriteria"); + + b3.Navigation("Tags"); + }); + + b2.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b3 => + { + b3.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b3.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b3.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "Id"); + + b3.ToTable("Budgets", "budget"); + + b3.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId"); + }); + + b2.Navigation("Subcriteria"); + + b2.Navigation("Tags"); + }); + + b1.Navigation("Subcriteria"); + + b1.Navigation("Tags"); + }); + + b.Navigation("LogbookCriteria") + .IsRequired(); + + b.Navigation("TaggingCriteria"); + + b.Navigation("TransferCriteria"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredCsvFileReadingOption", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", "Budget") + .WithMany("CsvReadingOptions") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredFieldConfiguration", "AttributesConfiguration", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Field") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("CsvFileReadingOptions_AttributesConfiguration", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredFieldConfiguration", "FieldConfigurations", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Field") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("CsvFileReadingOptions_FieldConfigurations", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredValidationRule", "ValidationRules", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Condition") + .HasColumnType("integer"); + + b1.Property("FieldConfiguration") + .IsRequired() + .HasColumnType("text"); + + b1.Property("RuleName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("StoredValidationRule", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.Navigation("AttributesConfiguration"); + + b.Navigation("Budget"); + + b.Navigation("FieldConfigurations"); + + b.Navigation("ValidationRules"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", "Budget") + .WithMany("Operations") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredMoney", "Amount", b1 => + { + b1.Property("StoredOperationId") + .HasColumnType("uuid"); + + b1.Property("Amount") + .HasColumnType("numeric"); + + b1.Property("Currency") + .HasColumnType("integer"); + + b1.HasKey("StoredOperationId"); + + b1.ToTable("Operations", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredOperationId"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b1 => + { + b1.Property("StoredOperationId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("StoredOperationId", "Id"); + + b1.ToTable("Operations_Tags", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredOperationId"); + }); + + b.Navigation("Amount") + .IsRequired(); + + b.Navigation("Budget"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredRate", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", "Sink") + .WithOne("SinkTransfer") + .HasForeignKey("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", "SinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", "Source") + .WithOne("SourceTransfer") + .HasForeignKey("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", "SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredMoney", "Fee", b1 => + { + b1.Property("StoredTransferId") + .HasColumnType("uuid"); + + b1.Property("Amount") + .HasColumnType("numeric"); + + b1.Property("Currency") + .HasColumnType("integer"); + + b1.HasKey("StoredTransferId"); + + b1.ToTable("Transfers", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredTransferId"); + }); + + b.Navigation("Fee") + .IsRequired(); + + b.Navigation("Sink"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("StoredBudgetStoredOwner", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", null) + .WithMany() + .HasForeignKey("BudgetsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.Navigation("CsvReadingOptions"); + + b.Navigation("Operations"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.Navigation("SinkTransfer"); + + b.Navigation("SourceTransfer"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.cs new file mode 100644 index 00000000..cd2e137f --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251005130019_RenameAccountToBudget.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Persistence.EF.Migrations +{ + /// + public partial class RenameAccountToBudget : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + /* odd. Generated but failed to apply + TODO investigate `42704: constraint "FK_StoredBudgetStoredOwner_Budgets_AccountsId" of relation "StoredBudgetStoredOwner" does not exist` error + migrationBuilder.DropForeignKey( + name: "FK_StoredBudgetStoredOwner_Budgets_AccountsId", + schema: "budget", + table: "StoredBudgetStoredOwner"); + */ + + migrationBuilder.RenameColumn( + name: "AccountsId", + schema: "budget", + table: "StoredBudgetStoredOwner", + newName: "BudgetsId"); + + migrationBuilder.AddForeignKey( + name: "FK_StoredBudgetStoredOwner_Budgets_BudgetsId", + schema: "budget", + table: "StoredBudgetStoredOwner", + column: "BudgetsId", + principalSchema: "budget", + principalTable: "Budgets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_StoredBudgetStoredOwner_Budgets_BudgetsId", + schema: "budget", + table: "StoredBudgetStoredOwner"); + + migrationBuilder.RenameColumn( + name: "BudgetsId", + schema: "budget", + table: "StoredBudgetStoredOwner", + newName: "AccountsId"); + + migrationBuilder.AddForeignKey( + name: "FK_StoredBudgetStoredOwner_Budgets_AccountsId", + schema: "budget", + table: "StoredBudgetStoredOwner", + column: "AccountsId", + principalSchema: "budget", + principalTable: "Budgets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.Designer.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.Designer.cs new file mode 100644 index 00000000..453450df --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.Designer.cs @@ -0,0 +1,1516 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NVs.Budget.Infrastructure.Persistence.EF.Context; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Persistence.EF.Migrations +{ + [DbContext(typeof(BudgetContext))] + [Migration("20251124055351_TransferDates")] + partial class TransferDates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("budget") + .HasAnnotation("ProductVersion", "8.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "currency_iso_code", new[] { "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", "bhd", "bif", "bmd", "bnd", "bob", "bov", "brl", "bsd", "btn", "bwp", "byn", "byr", "bzd", "cad", "cdf", "che", "chf", "chw", "clf", "clp", "cny", "cop", "cou", "crc", "cuc", "cup", "cve", "czk", "djf", "dkk", "dop", "dzd", "eek", "egp", "ern", "etb", "eur", "fjd", "fkp", "gbp", "gel", "ghs", "gip", "gmd", "gnf", "gtq", "gyd", "hkd", "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "iqd", "irr", "isk", "jmd", "jod", "jpy", "kes", "kgs", "khr", "kmf", "kpw", "krw", "kwd", "kyd", "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "ltl", "lvl", "lyd", "mad", "mdl", "mga", "mkd", "mmk", "mnt", "mop", "mro", "mru", "mur", "mvr", "mwk", "mxn", "mxv", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", "nzd", "omr", "pab", "pen", "pgk", "php", "pkr", "pln", "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", "scr", "sdg", "sek", "sgd", "shp", "sle", "sll", "sos", "srd", "ssp", "std", "stn", "svc", "syp", "szl", "thb", "tjs", "tmt", "tnd", "top", "try", "ttd", "twd", "tzs", "uah", "ugx", "usd", "usn", "uss", "uyi", "uyu", "uyw", "uzs", "ved", "vef", "ves", "vnd", "vuv", "wst", "xaf", "xag", "xau", "xba", "xbb", "xbc", "xbd", "xcd", "xdr", "xof", "xpd", "xpf", "xpt", "xsu", "xts", "xua", "xxx", "yer", "zar", "zmk", "zmw", "zwg", "zwl" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Budgets", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredCsvFileReadingOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CultureInfo") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateTimeKind") + .HasColumnType("integer"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("FileNamePattern") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.ToTable("CsvFileReadingOptions", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Attributes") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("BudgetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BudgetId"); + + b.ToTable("Operations", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Owners", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredRate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AsOf") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("From") + .HasColumnType("integer"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("To") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Rates", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("SinkId") + .HasColumnType("uuid"); + + b.Property("SourceId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SinkId") + .IsUnique(); + + b.HasIndex("SourceId") + .IsUnique(); + + b.ToTable("Transfers", "budget"); + }); + + modelBuilder.Entity("StoredBudgetStoredOwner", b => + { + b.Property("BudgetsId") + .HasColumnType("uuid"); + + b.Property("OwnersId") + .HasColumnType("uuid"); + + b.HasKey("BudgetsId", "OwnersId"); + + b.HasIndex("OwnersId"); + + b.ToTable("StoredBudgetStoredOwner", "budget"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTaggingCriterion", "TaggingCriteria", b1 => + { + b1.Property("BudgetId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Condition") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Tag") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BudgetId", "Id"); + + b1.ToTable("StoredTaggingCriterion", "budget"); + + b1.WithOwner("Budget") + .HasForeignKey("BudgetId"); + + b1.Navigation("Budget"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransferCriterion", "TransferCriteria", b1 => + { + b1.Property("BudgetId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Accuracy") + .HasColumnType("integer"); + + b1.Property("Comment") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Criterion") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("BudgetId", "Id"); + + b1.ToTable("StoredTransferCriterion", "budget"); + + b1.WithOwner("Budget") + .HasForeignKey("BudgetId"); + + b1.Navigation("Budget"); + }); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "LogbookCriteria", b1 => + { + b1.Property("StoredBudgetId") + .HasColumnType("uuid"); + + b1.Property("Criteria") + .HasColumnType("text"); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("IsUniversal") + .HasColumnType("boolean"); + + b1.Property("Substitution") + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("integer"); + + b1.HasKey("StoredBudgetId"); + + b1.ToTable("Budgets", "budget"); + + b1.ToJson("LogbookCriteria"); + + b1.WithOwner() + .HasForeignKey("StoredBudgetId"); + + b1.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b2 => + { + b2.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("StoredLogbookCriteriaStoredBudgetId", "Id"); + + b2.ToTable("Budgets", "budget"); + + b2.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId"); + }); + + b1.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria.Subcriteria#StoredLogbookCriteria", "Subcriteria", b2 => + { + b2.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b2.Property("Criteria") + .HasColumnType("text"); + + b2.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b2.Property("IsUniversal") + .HasColumnType("boolean"); + + b2.Property("Substitution") + .HasColumnType("text"); + + b2.Property("Type") + .HasColumnType("integer"); + + b2.HasKey("StoredLogbookCriteriaStoredBudgetId", "Id"); + + b2.ToTable("Budgets", "budget"); + + b2.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId"); + + b2.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b3 => + { + b3.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b3.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b3.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Criteria") + .HasColumnType("text"); + + b3.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b3.Property("IsUniversal") + .HasColumnType("boolean"); + + b3.Property("Substitution") + .HasColumnType("text"); + + b3.Property("Type") + .HasColumnType("integer"); + + b3.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "Id"); + + b3.ToTable("Budgets", "budget"); + + b3.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId"); + + b3.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b4 => + { + b4.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b4.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b4.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b4.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b4.Property("Criteria") + .HasColumnType("text"); + + b4.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b4.Property("IsUniversal") + .HasColumnType("boolean"); + + b4.Property("Substitution") + .HasColumnType("text"); + + b4.Property("Type") + .HasColumnType("integer"); + + b4.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "Id"); + + b4.ToTable("Budgets", "budget"); + + b4.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1"); + + b4.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b5 => + { + b5.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b5.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b5.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b5.Property("Criteria") + .HasColumnType("text"); + + b5.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b5.Property("IsUniversal") + .HasColumnType("boolean"); + + b5.Property("Substitution") + .HasColumnType("text"); + + b5.Property("Type") + .HasColumnType("integer"); + + b5.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "Id"); + + b5.ToTable("Budgets", "budget"); + + b5.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2"); + + b5.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b6 => + { + b6.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b6.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b6.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b6.Property("Criteria") + .HasColumnType("text"); + + b6.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b6.Property("IsUniversal") + .HasColumnType("boolean"); + + b6.Property("Substitution") + .HasColumnType("text"); + + b6.Property("Type") + .HasColumnType("integer"); + + b6.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "Id"); + + b6.ToTable("Budgets", "budget"); + + b6.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3"); + + b6.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b7 => + { + b7.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b7.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b7.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b7.Property("Criteria") + .HasColumnType("text"); + + b7.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b7.Property("IsUniversal") + .HasColumnType("boolean"); + + b7.Property("Substitution") + .HasColumnType("text"); + + b7.Property("Type") + .HasColumnType("integer"); + + b7.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "Id"); + + b7.ToTable("Budgets", "budget"); + + b7.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4"); + + b7.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b8 => + { + b8.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b8.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b8.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b8.Property("Criteria") + .HasColumnType("text"); + + b8.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b8.Property("IsUniversal") + .HasColumnType("boolean"); + + b8.Property("Substitution") + .HasColumnType("text"); + + b8.Property("Type") + .HasColumnType("integer"); + + b8.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "Id"); + + b8.ToTable("Budgets", "budget"); + + b8.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5"); + + b8.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b9 => + { + b9.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b9.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b9.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b9.Property("Criteria") + .HasColumnType("text"); + + b9.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b9.Property("IsUniversal") + .HasColumnType("boolean"); + + b9.Property("Substitution") + .HasColumnType("text"); + + b9.Property("Type") + .HasColumnType("integer"); + + b9.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "Id"); + + b9.ToTable("Budgets", "budget"); + + b9.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6"); + + b9.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b10 => + { + b10.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b10.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b10.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b10.Property("Criteria") + .HasColumnType("text"); + + b10.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b10.Property("IsUniversal") + .HasColumnType("boolean"); + + b10.Property("Substitution") + .HasColumnType("text"); + + b10.Property("Type") + .HasColumnType("integer"); + + b10.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "Id"); + + b10.ToTable("Budgets", "budget"); + + b10.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7"); + + b10.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredLogbookCriteria", "Subcriteria", b11 => + { + b11.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b11.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b11.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b11.Property("Criteria") + .HasColumnType("text"); + + b11.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b11.Property("IsUniversal") + .HasColumnType("boolean"); + + b11.Property("Substitution") + .HasColumnType("text"); + + b11.Property("Type") + .HasColumnType("integer"); + + b11.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "Id"); + + b11.ToTable("Budgets", "budget"); + + b11.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8"); + + b11.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b12 => + { + b12.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b12.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b12.Property("StoredLogbookCriteriaId9") + .HasColumnType("integer"); + + b12.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b12.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b12.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "StoredLogbookCriteriaId9", "Id"); + + b12.ToTable("Budgets", "budget"); + + b12.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "StoredLogbookCriteriaId9"); + }); + + b11.Navigation("Tags"); + }); + + b10.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b11 => + { + b11.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b11.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b11.Property("StoredLogbookCriteriaId8") + .HasColumnType("integer"); + + b11.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b11.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b11.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8", "Id"); + + b11.ToTable("Budgets", "budget"); + + b11.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "StoredLogbookCriteriaId8"); + }); + + b10.Navigation("Subcriteria"); + + b10.Navigation("Tags"); + }); + + b9.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b10 => + { + b10.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b10.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b10.Property("StoredLogbookCriteriaId7") + .HasColumnType("integer"); + + b10.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b10.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b10.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7", "Id"); + + b10.ToTable("Budgets", "budget"); + + b10.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "StoredLogbookCriteriaId7"); + }); + + b9.Navigation("Subcriteria"); + + b9.Navigation("Tags"); + }); + + b8.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b9 => + { + b9.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b9.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b9.Property("StoredLogbookCriteriaId6") + .HasColumnType("integer"); + + b9.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b9.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b9.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6", "Id"); + + b9.ToTable("Budgets", "budget"); + + b9.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "StoredLogbookCriteriaId6"); + }); + + b8.Navigation("Subcriteria"); + + b8.Navigation("Tags"); + }); + + b7.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b8 => + { + b8.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b8.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b8.Property("StoredLogbookCriteriaId5") + .HasColumnType("integer"); + + b8.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b8.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b8.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5", "Id"); + + b8.ToTable("Budgets", "budget"); + + b8.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "StoredLogbookCriteriaId5"); + }); + + b7.Navigation("Subcriteria"); + + b7.Navigation("Tags"); + }); + + b6.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b7 => + { + b7.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b7.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b7.Property("StoredLogbookCriteriaId4") + .HasColumnType("integer"); + + b7.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b7.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b7.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4", "Id"); + + b7.ToTable("Budgets", "budget"); + + b7.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "StoredLogbookCriteriaId4"); + }); + + b6.Navigation("Subcriteria"); + + b6.Navigation("Tags"); + }); + + b5.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b6 => + { + b6.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b6.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b6.Property("StoredLogbookCriteriaId3") + .HasColumnType("integer"); + + b6.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b6.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b6.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3", "Id"); + + b6.ToTable("Budgets", "budget"); + + b6.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "StoredLogbookCriteriaId3"); + }); + + b5.Navigation("Subcriteria"); + + b5.Navigation("Tags"); + }); + + b4.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b5 => + { + b5.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b5.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b5.Property("StoredLogbookCriteriaId2") + .HasColumnType("integer"); + + b5.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b5.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b5.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2", "Id"); + + b5.ToTable("Budgets", "budget"); + + b5.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "StoredLogbookCriteriaId2"); + }); + + b4.Navigation("Subcriteria"); + + b4.Navigation("Tags"); + }); + + b3.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b4 => + { + b4.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b4.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b4.Property("StoredLogbookCriteriaId1") + .HasColumnType("integer"); + + b4.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b4.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b4.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1", "Id"); + + b4.ToTable("Budgets", "budget"); + + b4.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "StoredLogbookCriteriaId1"); + }); + + b3.Navigation("Subcriteria"); + + b3.Navigation("Tags"); + }); + + b2.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b3 => + { + b3.Property("StoredLogbookCriteriaStoredBudgetId") + .HasColumnType("uuid"); + + b3.Property("StoredLogbookCriteriaId") + .HasColumnType("integer"); + + b3.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + b3.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b3.HasKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId", "Id"); + + b3.ToTable("Budgets", "budget"); + + b3.WithOwner() + .HasForeignKey("StoredLogbookCriteriaStoredBudgetId", "StoredLogbookCriteriaId"); + }); + + b2.Navigation("Subcriteria"); + + b2.Navigation("Tags"); + }); + + b1.Navigation("Subcriteria"); + + b1.Navigation("Tags"); + }); + + b.Navigation("LogbookCriteria") + .IsRequired(); + + b.Navigation("TaggingCriteria"); + + b.Navigation("TransferCriteria"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredCsvFileReadingOption", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", "Budget") + .WithMany("CsvReadingOptions") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredFieldConfiguration", "AttributesConfiguration", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Field") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("CsvFileReadingOptions_AttributesConfiguration", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredFieldConfiguration", "FieldConfigurations", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Field") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Pattern") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("CsvFileReadingOptions_FieldConfigurations", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredValidationRule", "ValidationRules", b1 => + { + b1.Property("FileReadingOptionId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Condition") + .HasColumnType("integer"); + + b1.Property("FieldConfiguration") + .IsRequired() + .HasColumnType("text"); + + b1.Property("RuleName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("FileReadingOptionId", "Id"); + + b1.ToTable("StoredValidationRule", "budget"); + + b1.WithOwner("FileReadingOption") + .HasForeignKey("FileReadingOptionId"); + + b1.Navigation("FileReadingOption"); + }); + + b.Navigation("AttributesConfiguration"); + + b.Navigation("Budget"); + + b.Navigation("FieldConfigurations"); + + b.Navigation("ValidationRules"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", "Budget") + .WithMany("Operations") + .HasForeignKey("BudgetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredMoney", "Amount", b1 => + { + b1.Property("StoredOperationId") + .HasColumnType("uuid"); + + b1.Property("Amount") + .HasColumnType("numeric"); + + b1.Property("Currency") + .HasColumnType("integer"); + + b1.HasKey("StoredOperationId"); + + b1.ToTable("Operations", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredOperationId"); + }); + + b.OwnsMany("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTag", "Tags", b1 => + { + b1.Property("StoredOperationId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("StoredOperationId", "Id"); + + b1.ToTable("Operations_Tags", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredOperationId"); + }); + + b.Navigation("Amount") + .IsRequired(); + + b.Navigation("Budget"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredRate", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", "Sink") + .WithOne("SinkTransfer") + .HasForeignKey("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", "SinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", "Source") + .WithOne("SourceTransfer") + .HasForeignKey("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredTransfer", "SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredMoney", "Fee", b1 => + { + b1.Property("StoredTransferId") + .HasColumnType("uuid"); + + b1.Property("Amount") + .HasColumnType("numeric"); + + b1.Property("Currency") + .HasColumnType("integer"); + + b1.HasKey("StoredTransferId"); + + b1.ToTable("Transfers", "budget"); + + b1.WithOwner() + .HasForeignKey("StoredTransferId"); + }); + + b.Navigation("Fee") + .IsRequired(); + + b.Navigation("Sink"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("StoredBudgetStoredOwner", b => + { + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", null) + .WithMany() + .HasForeignKey("BudgetsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOwner", null) + .WithMany() + .HasForeignKey("OwnersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", b => + { + b.Navigation("CsvReadingOptions"); + + b.Navigation("Operations"); + }); + + modelBuilder.Entity("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredOperation", b => + { + b.Navigation("SinkTransfer"); + + b.Navigation("SourceTransfer"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.cs new file mode 100644 index 00000000..52ab90e3 --- /dev/null +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/20251124055351_TransferDates.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NVs.Budget.Infrastructure.Persistence.EF.Migrations +{ + /// + public partial class TransferDates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CompletedAt", + schema: "budget", + table: "Transfers", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "StartedAt", + schema: "budget", + table: "Transfers", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CompletedAt", + schema: "budget", + table: "Transfers"); + + migrationBuilder.DropColumn( + name: "StartedAt", + schema: "budget", + table: "Transfers"); + } + } +} diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs similarity index 99% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs index e68481f4..f95c6315 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Migrations/BudgetContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("budget") - .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("ProductVersion", "8.0.13") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "currency_iso_code", new[] { "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", "bhd", "bif", "bmd", "bnd", "bob", "bov", "brl", "bsd", "btn", "bwp", "byn", "byr", "bzd", "cad", "cdf", "che", "chf", "chw", "clf", "clp", "cny", "cop", "cou", "crc", "cuc", "cup", "cve", "czk", "djf", "dkk", "dop", "dzd", "eek", "egp", "ern", "etb", "eur", "fjd", "fkp", "gbp", "gel", "ghs", "gip", "gmd", "gnf", "gtq", "gyd", "hkd", "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "iqd", "irr", "isk", "jmd", "jod", "jpy", "kes", "kgs", "khr", "kmf", "kpw", "krw", "kwd", "kyd", "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "ltl", "lvl", "lyd", "mad", "mdl", "mga", "mkd", "mmk", "mnt", "mop", "mro", "mru", "mur", "mvr", "mwk", "mxn", "mxv", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", "nzd", "omr", "pab", "pen", "pgk", "php", "pkr", "pln", "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", "scr", "sdg", "sek", "sgd", "shp", "sle", "sll", "sos", "srd", "ssp", "std", "stn", "svc", "syp", "szl", "thb", "tjs", "tmt", "tnd", "top", "try", "ttd", "twd", "tzs", "uah", "ugx", "usd", "usn", "uss", "uyi", "uyu", "uyw", "uzs", "ved", "vef", "ves", "vnd", "vuv", "wst", "xaf", "xag", "xau", "xba", "xbb", "xbc", "xbd", "xcd", "xdr", "xof", "xpd", "xpf", "xpt", "xsu", "xts", "xua", "xxx", "yer", "zar", "zmk", "zmw", "zwg", "zwl" }); @@ -206,6 +206,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -218,6 +221,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SourceId") .HasColumnType("uuid"); + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -234,13 +240,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("StoredBudgetStoredOwner", b => { - b.Property("AccountsId") + b.Property("BudgetsId") .HasColumnType("uuid"); b.Property("OwnersId") .HasColumnType("uuid"); - b.HasKey("AccountsId", "OwnersId"); + b.HasKey("BudgetsId", "OwnersId"); b.HasIndex("OwnersId"); @@ -377,7 +383,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b2.Property("IsUniversal") + b2.Property("IsUniversal") .HasColumnType("boolean"); b2.Property("Substitution") @@ -412,7 +418,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b3.Property("IsUniversal") + b3.Property("IsUniversal") .HasColumnType("boolean"); b3.Property("Substitution") @@ -450,7 +456,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b4.Property("IsUniversal") + b4.Property("IsUniversal") .HasColumnType("boolean"); b4.Property("Substitution") @@ -491,7 +497,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b5.Property("IsUniversal") + b5.Property("IsUniversal") .HasColumnType("boolean"); b5.Property("Substitution") @@ -535,7 +541,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b6.Property("IsUniversal") + b6.Property("IsUniversal") .HasColumnType("boolean"); b6.Property("Substitution") @@ -582,7 +588,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b7.Property("IsUniversal") + b7.Property("IsUniversal") .HasColumnType("boolean"); b7.Property("Substitution") @@ -632,7 +638,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b8.Property("IsUniversal") + b8.Property("IsUniversal") .HasColumnType("boolean"); b8.Property("Substitution") @@ -685,7 +691,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b9.Property("IsUniversal") + b9.Property("IsUniversal") .HasColumnType("boolean"); b9.Property("Substitution") @@ -741,7 +747,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b10.Property("IsUniversal") + b10.Property("IsUniversal") .HasColumnType("boolean"); b10.Property("Substitution") @@ -800,7 +806,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b11.Property("IsUniversal") + b11.Property("IsUniversal") .HasColumnType("boolean"); b11.Property("Substitution") @@ -1477,7 +1483,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("NVs.Budget.Infrastructure.Persistence.EF.Entities.StoredBudget", null) .WithMany() - .HasForeignKey("AccountsId") + .HasForeignKey("BudgetsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj similarity index 57% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj index 249bac00..005bcf70 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/NVs.Budget.Infrastructure.Persistence.EF.csproj @@ -14,21 +14,19 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsFinder.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsFinder.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsFinder.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsFinder.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs similarity index 97% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs index 992a6e51..53fa1543 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/BudgetsRepository.cs @@ -2,8 +2,8 @@ using AutoMapper; using FluentResults; using Microsoft.EntityFrameworkCore; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; using NVs.Budget.Infrastructure.Persistence.EF.Context; using NVs.Budget.Infrastructure.Persistence.EF.Entities; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs similarity index 88% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs index c3de1262..6681b9fe 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExchangeRatesRepository.cs @@ -2,7 +2,7 @@ using AutoMapper; using Microsoft.EntityFrameworkCore; using NMoneys; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; using NVs.Budget.Infrastructure.Persistence.EF.Context; @@ -14,6 +14,7 @@ internal class ExchangeRatesRepository(BudgetContext context, IMapper mapper) : { public async Task GetRate(Owner owner, DateTime asOf, Currency from, Currency to, CancellationToken ct) { + asOf = asOf.ToUniversalTime(); Expression> criteria = r => r.Owner.Id == owner.Id && r.From == from.IsoCode && r.To == to.IsoCode && r.AsOf >= r.AsOf.Date && r.AsOf <= asOf; @@ -25,7 +26,7 @@ internal class ExchangeRatesRepository(BudgetContext context, IMapper mapper) : public async Task Add(ExchangeRate rate, Owner owner, CancellationToken ct) { var storedOwner = await context.Owners.FirstOrDefaultAsync(o => o.Id == owner.Id, ct) ?? throw new InvalidOperationException("Owner is not registered yet! Register owner first!"); - await context.Rates.AddAsync(new(rate.AsOf, rate.From.IsoCode, rate.To.IsoCode, rate.Rate) + await context.Rates.AddAsync(new(rate.AsOf.ToUniversalTime(), rate.From.IsoCode, rate.To.IsoCode, rate.Rate) { Owner = storedOwner }, ct); diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallExcludingVisitor.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallExcludingVisitor.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallExcludingVisitor.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallExcludingVisitor.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallRewritingVisitor.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallRewritingVisitor.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallRewritingVisitor.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/DictionaryCallRewritingVisitor.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/ExpressionSplitter.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/ExpressionSplitter.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/ExpressionSplitter.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/ExpressionVisitors/ExpressionSplitter.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs similarity index 97% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs index c475dc5d..6458431e 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OperationsRepository.cs @@ -4,9 +4,8 @@ using FluentResults; using Microsoft.EntityFrameworkCore; using NMoneys; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Application.Contracts.Errors.Accounting; -using NVs.Budget.Application.Services.Accounting.Transfers; using NVs.Budget.Domain.ValueObjects; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; using NVs.Budget.Infrastructure.Persistence.EF.Context; @@ -80,7 +79,7 @@ public IAsyncEnumerable> Register(IAsyncEnumerable> Update(IAsyncEnumerable operations, [EnumeratorCancellation] CancellationToken ct) { var queue = new Queue(); - await foreach (var u in operations.Select(TransferTags.Untag).WithCancellation(ct)) + await foreach (var u in operations.WithCancellation(ct)) { queue.Enqueue(u); if (queue.Count > BatchSize) diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs similarity index 95% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs index d03d5b7d..40534fa1 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/OwnersRepository.cs @@ -3,8 +3,8 @@ using FluentResults; using Microsoft.EntityFrameworkCore; using NVs.Budget.Application.Contracts.Entities; -using NVs.Budget.Application.Contracts.Entities.Budgeting; -using NVs.Budget.Domain.Entities.Accounts; +using NVs.Budget.Application.Contracts.Entities.Accounting; +using NVs.Budget.Domain.Entities.Budgets; using NVs.Budget.Domain.Extensions; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; using NVs.Budget.Infrastructure.Persistence.EF.Context; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/RepositoryBase.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/RepositoryBase.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/RepositoryBase.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/RepositoryBase.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs similarity index 63% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs index df60d32b..27394c60 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/BudgetDoesNotExistsError.cs @@ -1,8 +1,8 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.EF.Repositories.Results; -internal class BudgetDoesNotExistsError(Domain.Entities.Accounts.Budget budget) +internal class BudgetDoesNotExistsError(Domain.Entities.Budgets.Budget budget) : ErrorBase("Budget with given id does not exists", new Dictionary { diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs similarity index 81% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs index 0a69ccd5..590935c7 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/CannotChangeBudgetError.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; namespace NVs.Budget.Infrastructure.Persistence.EF.Repositories.Results; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/EntityDoesNotExistError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/EntityDoesNotExistError.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/EntityDoesNotExistError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/EntityDoesNotExistError.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/ErrorBase.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/ErrorBase.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/ErrorBase.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/ErrorBase.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsAlreadyRegisteredError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsAlreadyRegisteredError.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsAlreadyRegisteredError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsAlreadyRegisteredError.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsNotRegisteredError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsNotRegisteredError.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsNotRegisteredError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/OwnerIsNotRegisteredError.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs similarity index 84% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs index f67b1fe4..0b4361bc 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/TransferAlreadyRegisteredError.cs @@ -1,4 +1,4 @@ -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.Entities.Transactions; namespace NVs.Budget.Infrastructure.Persistence.EF.Repositories.Results; diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/VersionDoesNotMatchError.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/VersionDoesNotMatchError.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/VersionDoesNotMatchError.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/Results/VersionDoesNotMatchError.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs similarity index 95% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs index dc0b0559..94a9c095 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs +++ b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/TransfersRepository.cs @@ -2,7 +2,7 @@ using AutoMapper; using FluentResults; using Microsoft.EntityFrameworkCore; -using NVs.Budget.Application.Contracts.Entities.Budgeting; +using NVs.Budget.Application.Contracts.Entities.Accounting; using NVs.Budget.Domain.Entities.Operations; using NVs.Budget.Infrastructure.Persistence.Contracts.Accounting; using NVs.Budget.Infrastructure.Persistence.EF.Context; @@ -28,7 +28,7 @@ public IAsyncEnumerable Get(Expression t.Source).ThenInclude(o => o.Budget).ThenInclude(a => a.Owners) .Include(t => t.Sink).ThenInclude(o => o.Budget).ThenInclude(a => a.Owners) .AsNoTracking() - .Where(queryable) + .Where(queryable.CombineWith(t => !t.Deleted && !t.Source.Deleted && !t.Sink.Deleted)) .AsSplitQuery() .AsAsyncEnumerable() .Where(enumerable) diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/VersionGenerator.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/VersionGenerator.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/Repositories/VersionGenerator.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/Repositories/VersionGenerator.cs diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/globals.cs b/src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/globals.cs similarity index 100% rename from src/Infrastructure/NVs.Budget.Infrastructure.Persistence.EF/globals.cs rename to src/Infrastructure/Persistence/NVs.Budget.Infrastructure.Persistence.EF/globals.cs diff --git a/src/NVs.Budget.sln b/src/NVs.Budget.sln index eb06beb6..e6a3f617 100644 --- a/src/NVs.Budget.sln +++ b/src/NVs.Budget.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1. Application", "1. Application", "{4D906D63-8704-470A-B3BF-64A314A8B1A3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Domain", "Domain\NVs.Budget.Domain\NVs.Budget.Domain.csproj", "{A757A6B7-3317-493A-B9CB-DA97CCCA6A7C}" @@ -18,10 +17,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Expres EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Infrastructure", "2. Infrastructure", "{BBC78D28-4FE0-4044-8540-5E026D8BACB9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.EF", "Infrastructure\NVs.Budget.Infrastructure.Persistence.EF\NVs.Budget.Infrastructure.Persistence.EF.csproj", "{8BE1189F-A57A-4904-B167-675F3E4EBA3E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.EF.Tests", "Infrastructure\NVs.Budget.Infrastructure.Persistence.EF.Tests\NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj", "{C955D374-83C1-43CC-9778-5A58EFFCF949}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Application.Contracts", "Application\NVs.Budget.Application.Contracts\NVs.Budget.Application.Contracts.csproj", "{68893284-DE40-4B39-A8EE-ED851DC5116B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.Contracts", "Application\NVs.Budget.Infrastructure.Persistence.Contracts\NVs.Budget.Infrastructure.Persistence.Contracts.csproj", "{8CD10A15-F1DD-40EC-87F4-9983ED589B07}" @@ -36,38 +31,49 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.I EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3. Controllers", "3. Controllers", "{8FBC3B88-0C56-4282-BBF3-607292E95F91}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Identity.Console", "Infrastructure\NVs.Budget.Infrastructure.Identity.Console\NVs.Budget.Infrastructure.Identity.Console.csproj", "{34D1CB73-E049-436A-97AD-E7329DEDF219}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Controllers.Console.Handlers", "Controllers\NVs.Budget.Controllers.Console.Handlers\NVs.Budget.Controllers.Console.Handlers.csproj", "{58C9DA84-921C-4749-9B6A-BC597B87EC77}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "InfrasturctureContracts", "InfrasturctureContracts", "{68B96F7C-5FBE-46BF-B40C-95CDC2DDC72E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Application.UseCases", "Application\NVs.Budget.Application.UseCases\NVs.Budget.Application.UseCases.csproj", "{AE89B9CE-525A-4C62-84D7-E546E78BA4E2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4. Hosts", "4. Hosts", "{ABCBF4A8-F237-4243-9A07-27D6B361CCCB}" - ProjectSection(SolutionItems) = preProject - Hosts\docker-compose.yml = Hosts\docker-compose.yml - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Hosts.Console", "Hosts\NVs.Budget.Hosts.Console\NVs.Budget.Hosts.Console.csproj", "{1DD67B12-0D3F-4CB6-9096-FCB1C341CB76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Controllers.Console.Handlers.Tests", "Controllers\NVs.Budget.Controllers.Console.Handlers.Tests\NVs.Budget.Controllers.Console.Handlers.Tests.csproj", "{0043D746-D5F8-482D-BF68-B969FCB5C544}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.MediatR", "Utilities\NVs.Budget.Utilities.MediatR\NVs.Budget.Utilities.MediatR.csproj", "{0BBD98E3-394D-444B-948C-15D883B1C4EC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Controllers.Console.Contracts", "Controllers\NVs.Budget.Controllers.Console.Contracts\NVs.Budget.Controllers.Console.Contracts.csproj", "{E72B5C99-FD5D-406C-9A19-769428D2E1A2}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Json", "Utilities\NVs.Budget.Utilities.Json\NVs.Budget.Utilities.Json.csproj", "{1D1E1A55-38A0-41AF-983E-A286F903B8FF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Json.Tests", "Utilities\NVs.Budget.Utilities.Json.Tests\NVs.Budget.Utilities.Json.Tests.csproj", "{B4FE361D-B8F8-4FBB-99B3-7EE5CF65BBF3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.IO.Console.Contracts", "Infrastructure\NVs.Budget.Infrastructure.IO.Console.Contracts\NVs.Budget.Infrastructure.IO.Console.Contracts.csproj", "{665E81AE-13CE-438B-8559-5CA0619F94A2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Expressions.Tests", "Utilities\NVs.Budget.Utilities.Expressions.Tests\NVs.Budget.Utilities.Expressions.Tests.csproj", "{7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.IO.Console", "Infrastructure\NVs.Budget.Infrastructure.IO.Console\NVs.Budget.Infrastructure.IO.Console.csproj", "{E7257A6C-B2C0-4B24-99EF-6E4E495FCF90}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Hosts.Web.Server", "Hosts\NVs.Budget.Hosts.Web.Server\NVs.Budget.Hosts.Web.Server.csproj", "{A129265C-EC73-4D2B-A4E8-E7DB6578D7B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.IO.Console.Tests", "Infrastructure\NVs.Budget.Infrastructure.IO.Console.Tests\NVs.Budget.Infrastructure.IO.Console.Tests.csproj", "{2A15D8BF-AD28-4192-A9E7-B81B8612B2CF}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C12A475A-DE78-41F0-A270-A759D23E9403}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Expressions.Tests", "Utilities\NVs.Budget.Utilities.Expressions.Tests\NVs.Budget.Utilities.Expressions.Tests.csproj", "{7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex", "Infrastructure\NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex\NVs.Budget.Infrastructure.Identity.OpenIddict.Yandex.csproj", "{A090F5DB-DD45-4A1F-844B-EF529B0E56A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Controllers.Web", "Controllers\NVs.Budget.Controllers.Web\NVs.Budget.Controllers.Web.csproj", "{EB175F71-CAA9-496A-839A-6597EC28CE7D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Hosts.Web.Client", "Hosts\NVs.Budget.Hosts.Web.Client\NVs.Budget.Hosts.Web.Client.csproj", "{8F054687-62ED-4A70-88FC-5D868673758F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Controllers.Web.Tests", "Controllers\NVs.Budget.Controllers.Web.Tests\NVs.Budget.Controllers.Web.Tests.csproj", "{28534653-368D-47F5-8C29-44D7D92C9AAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Utilities.Yaml", "Utilities\NVs.Budget.Utilities.Yaml\NVs.Budget.Utilities.Yaml.csproj", "{7E970918-EA3C-4794-B36F-E546521FA8A3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Persistence", "Persistence", "{63CCE4A9-0671-4691-845F-55D1AC1F2F9C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.EF", "Infrastructure\Persistence\NVs.Budget.Infrastructure.Persistence.EF\NVs.Budget.Infrastructure.Persistence.EF.csproj", "{D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.EF.Common", "Infrastructure\Persistence\NVs.Budget.Infrastructure.Persistence.EF.Common\NVs.Budget.Infrastructure.Persistence.EF.Common.csproj", "{48989170-0B67-4D0C-984C-898C23CDF732}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Persistence.EF.Tests", "Infrastructure\Persistence\NVs.Budget.Infrastructure.Persistence.EF.Tests\NVs.Budget.Infrastructure.Persistence.EF.Tests.csproj", "{6DB6B15D-837E-4B88-8234-CE79811D6CCF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{63BD943D-0D12-4C50-A4DB-3F86BBFC0396}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Files.CSV.Contracts", "Infrastructure\Files\NVs.Budget.Infrastructure.Files.CSV.Contracts\NVs.Budget.Infrastructure.Files.CSV.Contracts.csproj", "{E9FA870F-88B4-49A3-8FC3-514DF54B23A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Files.CSV", "Infrastructure\Files\NVs.Budget.Infrastructure.Files.CSV\NVs.Budget.Infrastructure.Files.CSV.csproj", "{1F75A498-5698-41E4-B8B6-3C5035D4BAC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NVs.Budget.Infrastructure.Files.CSV.Tests", "Infrastructure\Files\NVs.Budget.Infrastructure.Files.CSV.Tests\NVs.Budget.Infrastructure.Files.CSV.Tests.csproj", "{1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -79,29 +85,33 @@ Global {FB25F31F-C6D4-4A28-94A1-06E859713387} = {4D906D63-8704-470A-B3BF-64A314A8B1A3} {74A76BDE-3BE1-40A8-A8BA-C4CD9716DBBF} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} {DFAB3D6A-026F-4A5C-B845-CB8CEC6CE9DC} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} - {8BE1189F-A57A-4904-B167-675F3E4EBA3E} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} - {C955D374-83C1-43CC-9778-5A58EFFCF949} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} {68893284-DE40-4B39-A8EE-ED851DC5116B} = {4D906D63-8704-470A-B3BF-64A314A8B1A3} {A757A6B7-3317-493A-B9CB-DA97CCCA6A7C} = {DBF42B89-D4A0-4968-8C98-1ED3283B4188} {923B5464-1067-433F-9DD2-38183F146C83} = {DBF42B89-D4A0-4968-8C98-1ED3283B4188} {0DE2237D-B132-4B17-AC5A-CDDF04F5C95E} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} - {34D1CB73-E049-436A-97AD-E7329DEDF219} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} - {58C9DA84-921C-4749-9B6A-BC597B87EC77} = {8FBC3B88-0C56-4282-BBF3-607292E95F91} {68B96F7C-5FBE-46BF-B40C-95CDC2DDC72E} = {4D906D63-8704-470A-B3BF-64A314A8B1A3} {5A6CE7F3-C7E2-4DA4-BDE8-2BBFDE4A9B22} = {68B96F7C-5FBE-46BF-B40C-95CDC2DDC72E} {6B594AB9-45F8-4FA6-998A-C03C3E099429} = {68B96F7C-5FBE-46BF-B40C-95CDC2DDC72E} {8CD10A15-F1DD-40EC-87F4-9983ED589B07} = {68B96F7C-5FBE-46BF-B40C-95CDC2DDC72E} {AE89B9CE-525A-4C62-84D7-E546E78BA4E2} = {4D906D63-8704-470A-B3BF-64A314A8B1A3} - {1DD67B12-0D3F-4CB6-9096-FCB1C341CB76} = {ABCBF4A8-F237-4243-9A07-27D6B361CCCB} - {0043D746-D5F8-482D-BF68-B969FCB5C544} = {8FBC3B88-0C56-4282-BBF3-607292E95F91} {0BBD98E3-394D-444B-948C-15D883B1C4EC} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} - {E72B5C99-FD5D-406C-9A19-769428D2E1A2} = {8FBC3B88-0C56-4282-BBF3-607292E95F91} {1D1E1A55-38A0-41AF-983E-A286F903B8FF} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} {B4FE361D-B8F8-4FBB-99B3-7EE5CF65BBF3} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} - {665E81AE-13CE-438B-8559-5CA0619F94A2} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} - {E7257A6C-B2C0-4B24-99EF-6E4E495FCF90} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} - {2A15D8BF-AD28-4192-A9E7-B81B8612B2CF} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} {7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} + {A129265C-EC73-4D2B-A4E8-E7DB6578D7B5} = {ABCBF4A8-F237-4243-9A07-27D6B361CCCB} + {A090F5DB-DD45-4A1F-844B-EF529B0E56A6} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} + {EB175F71-CAA9-496A-839A-6597EC28CE7D} = {8FBC3B88-0C56-4282-BBF3-607292E95F91} + {8F054687-62ED-4A70-88FC-5D868673758F} = {ABCBF4A8-F237-4243-9A07-27D6B361CCCB} + {28534653-368D-47F5-8C29-44D7D92C9AAA} = {8FBC3B88-0C56-4282-BBF3-607292E95F91} + {7E970918-EA3C-4794-B36F-E546521FA8A3} = {52A892E9-EDF9-4297-9447-58C343C4EAF4} + {63CCE4A9-0671-4691-845F-55D1AC1F2F9C} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} + {D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7} = {63CCE4A9-0671-4691-845F-55D1AC1F2F9C} + {48989170-0B67-4D0C-984C-898C23CDF732} = {63CCE4A9-0671-4691-845F-55D1AC1F2F9C} + {6DB6B15D-837E-4B88-8234-CE79811D6CCF} = {63CCE4A9-0671-4691-845F-55D1AC1F2F9C} + {63BD943D-0D12-4C50-A4DB-3F86BBFC0396} = {BBC78D28-4FE0-4044-8540-5E026D8BACB9} + {E9FA870F-88B4-49A3-8FC3-514DF54B23A0} = {63BD943D-0D12-4C50-A4DB-3F86BBFC0396} + {1F75A498-5698-41E4-B8B6-3C5035D4BAC0} = {63BD943D-0D12-4C50-A4DB-3F86BBFC0396} + {1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A} = {63BD943D-0D12-4C50-A4DB-3F86BBFC0396} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A757A6B7-3317-493A-B9CB-DA97CCCA6A7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -128,14 +138,6 @@ Global {DFAB3D6A-026F-4A5C-B845-CB8CEC6CE9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFAB3D6A-026F-4A5C-B845-CB8CEC6CE9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {DFAB3D6A-026F-4A5C-B845-CB8CEC6CE9DC}.Release|Any CPU.Build.0 = Release|Any CPU - {8BE1189F-A57A-4904-B167-675F3E4EBA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8BE1189F-A57A-4904-B167-675F3E4EBA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8BE1189F-A57A-4904-B167-675F3E4EBA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8BE1189F-A57A-4904-B167-675F3E4EBA3E}.Release|Any CPU.Build.0 = Release|Any CPU - {C955D374-83C1-43CC-9778-5A58EFFCF949}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C955D374-83C1-43CC-9778-5A58EFFCF949}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C955D374-83C1-43CC-9778-5A58EFFCF949}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C955D374-83C1-43CC-9778-5A58EFFCF949}.Release|Any CPU.Build.0 = Release|Any CPU {68893284-DE40-4B39-A8EE-ED851DC5116B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68893284-DE40-4B39-A8EE-ED851DC5116B}.Debug|Any CPU.Build.0 = Debug|Any CPU {68893284-DE40-4B39-A8EE-ED851DC5116B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -156,34 +158,14 @@ Global {6B594AB9-45F8-4FA6-998A-C03C3E099429}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B594AB9-45F8-4FA6-998A-C03C3E099429}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B594AB9-45F8-4FA6-998A-C03C3E099429}.Release|Any CPU.Build.0 = Release|Any CPU - {34D1CB73-E049-436A-97AD-E7329DEDF219}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34D1CB73-E049-436A-97AD-E7329DEDF219}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34D1CB73-E049-436A-97AD-E7329DEDF219}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34D1CB73-E049-436A-97AD-E7329DEDF219}.Release|Any CPU.Build.0 = Release|Any CPU - {58C9DA84-921C-4749-9B6A-BC597B87EC77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58C9DA84-921C-4749-9B6A-BC597B87EC77}.Debug|Any CPU.Build.0 = Debug|Any CPU - {58C9DA84-921C-4749-9B6A-BC597B87EC77}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58C9DA84-921C-4749-9B6A-BC597B87EC77}.Release|Any CPU.Build.0 = Release|Any CPU {AE89B9CE-525A-4C62-84D7-E546E78BA4E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE89B9CE-525A-4C62-84D7-E546E78BA4E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE89B9CE-525A-4C62-84D7-E546E78BA4E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE89B9CE-525A-4C62-84D7-E546E78BA4E2}.Release|Any CPU.Build.0 = Release|Any CPU - {1DD67B12-0D3F-4CB6-9096-FCB1C341CB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1DD67B12-0D3F-4CB6-9096-FCB1C341CB76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1DD67B12-0D3F-4CB6-9096-FCB1C341CB76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1DD67B12-0D3F-4CB6-9096-FCB1C341CB76}.Release|Any CPU.Build.0 = Release|Any CPU - {0043D746-D5F8-482D-BF68-B969FCB5C544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0043D746-D5F8-482D-BF68-B969FCB5C544}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0043D746-D5F8-482D-BF68-B969FCB5C544}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0043D746-D5F8-482D-BF68-B969FCB5C544}.Release|Any CPU.Build.0 = Release|Any CPU {0BBD98E3-394D-444B-948C-15D883B1C4EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BBD98E3-394D-444B-948C-15D883B1C4EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BBD98E3-394D-444B-948C-15D883B1C4EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BBD98E3-394D-444B-948C-15D883B1C4EC}.Release|Any CPU.Build.0 = Release|Any CPU - {E72B5C99-FD5D-406C-9A19-769428D2E1A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E72B5C99-FD5D-406C-9A19-769428D2E1A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E72B5C99-FD5D-406C-9A19-769428D2E1A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E72B5C99-FD5D-406C-9A19-769428D2E1A2}.Release|Any CPU.Build.0 = Release|Any CPU {1D1E1A55-38A0-41AF-983E-A286F903B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1D1E1A55-38A0-41AF-983E-A286F903B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D1E1A55-38A0-41AF-983E-A286F903B8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -192,21 +174,57 @@ Global {B4FE361D-B8F8-4FBB-99B3-7EE5CF65BBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4FE361D-B8F8-4FBB-99B3-7EE5CF65BBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4FE361D-B8F8-4FBB-99B3-7EE5CF65BBF3}.Release|Any CPU.Build.0 = Release|Any CPU - {665E81AE-13CE-438B-8559-5CA0619F94A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {665E81AE-13CE-438B-8559-5CA0619F94A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {665E81AE-13CE-438B-8559-5CA0619F94A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {665E81AE-13CE-438B-8559-5CA0619F94A2}.Release|Any CPU.Build.0 = Release|Any CPU - {E7257A6C-B2C0-4B24-99EF-6E4E495FCF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7257A6C-B2C0-4B24-99EF-6E4E495FCF90}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7257A6C-B2C0-4B24-99EF-6E4E495FCF90}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7257A6C-B2C0-4B24-99EF-6E4E495FCF90}.Release|Any CPU.Build.0 = Release|Any CPU - {2A15D8BF-AD28-4192-A9E7-B81B8612B2CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A15D8BF-AD28-4192-A9E7-B81B8612B2CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A15D8BF-AD28-4192-A9E7-B81B8612B2CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A15D8BF-AD28-4192-A9E7-B81B8612B2CF}.Release|Any CPU.Build.0 = Release|Any CPU {7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D3E1EE7-0B18-4C90-AFAB-DB5A6D6D433B}.Release|Any CPU.Build.0 = Release|Any CPU + {A129265C-EC73-4D2B-A4E8-E7DB6578D7B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A129265C-EC73-4D2B-A4E8-E7DB6578D7B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A129265C-EC73-4D2B-A4E8-E7DB6578D7B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A129265C-EC73-4D2B-A4E8-E7DB6578D7B5}.Release|Any CPU.Build.0 = Release|Any CPU + {A090F5DB-DD45-4A1F-844B-EF529B0E56A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A090F5DB-DD45-4A1F-844B-EF529B0E56A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A090F5DB-DD45-4A1F-844B-EF529B0E56A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A090F5DB-DD45-4A1F-844B-EF529B0E56A6}.Release|Any CPU.Build.0 = Release|Any CPU + {EB175F71-CAA9-496A-839A-6597EC28CE7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB175F71-CAA9-496A-839A-6597EC28CE7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB175F71-CAA9-496A-839A-6597EC28CE7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB175F71-CAA9-496A-839A-6597EC28CE7D}.Release|Any CPU.Build.0 = Release|Any CPU + {8F054687-62ED-4A70-88FC-5D868673758F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F054687-62ED-4A70-88FC-5D868673758F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F054687-62ED-4A70-88FC-5D868673758F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F054687-62ED-4A70-88FC-5D868673758F}.Release|Any CPU.Build.0 = Release|Any CPU + {28534653-368D-47F5-8C29-44D7D92C9AAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28534653-368D-47F5-8C29-44D7D92C9AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28534653-368D-47F5-8C29-44D7D92C9AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28534653-368D-47F5-8C29-44D7D92C9AAA}.Release|Any CPU.Build.0 = Release|Any CPU + {7E970918-EA3C-4794-B36F-E546521FA8A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E970918-EA3C-4794-B36F-E546521FA8A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E970918-EA3C-4794-B36F-E546521FA8A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E970918-EA3C-4794-B36F-E546521FA8A3}.Release|Any CPU.Build.0 = Release|Any CPU + {D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D07E9C5C-3FDB-4458-8E88-D4DCF867ECD7}.Release|Any CPU.Build.0 = Release|Any CPU + {48989170-0B67-4D0C-984C-898C23CDF732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48989170-0B67-4D0C-984C-898C23CDF732}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48989170-0B67-4D0C-984C-898C23CDF732}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48989170-0B67-4D0C-984C-898C23CDF732}.Release|Any CPU.Build.0 = Release|Any CPU + {6DB6B15D-837E-4B88-8234-CE79811D6CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DB6B15D-837E-4B88-8234-CE79811D6CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DB6B15D-837E-4B88-8234-CE79811D6CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DB6B15D-837E-4B88-8234-CE79811D6CCF}.Release|Any CPU.Build.0 = Release|Any CPU + {E9FA870F-88B4-49A3-8FC3-514DF54B23A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9FA870F-88B4-49A3-8FC3-514DF54B23A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9FA870F-88B4-49A3-8FC3-514DF54B23A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9FA870F-88B4-49A3-8FC3-514DF54B23A0}.Release|Any CPU.Build.0 = Release|Any CPU + {1F75A498-5698-41E4-B8B6-3C5035D4BAC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F75A498-5698-41E4-B8B6-3C5035D4BAC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F75A498-5698-41E4-B8B6-3C5035D4BAC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F75A498-5698-41E4-B8B6-3C5035D4BAC0}.Release|Any CPU.Build.0 = Release|Any CPU + {1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FDB9523-4A5F-40DA-971A-E9F63A6F5D5A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection -EndGlobal +EndGlobal \ No newline at end of file diff --git a/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/NVs.Budget.Utilities.Expressions.Tests.csproj b/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/NVs.Budget.Utilities.Expressions.Tests.csproj index dad97fbf..2e8f353a 100644 --- a/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/NVs.Budget.Utilities.Expressions.Tests.csproj +++ b/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/NVs.Budget.Utilities.Expressions.Tests.csproj @@ -17,7 +17,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/ReplaceTypeVisitorShould.cs b/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/ReplaceTypeVisitorShould.cs new file mode 100644 index 00000000..16b98f3a --- /dev/null +++ b/src/Utilities/NVs.Budget.Utilities.Expressions.Tests/ReplaceTypeVisitorShould.cs @@ -0,0 +1,35 @@ +using System.Linq.Expressions; +using NVs.Budget.Utilities.Expressions; + +namespace NVs.Budget.Utilities.Expressions.Tests; + +public class ReplaceTypeVisitorShould +{ + private sealed class Source + { + public Guid Id { get; init; } + } + + private sealed class Target + { + public Guid Id { get; init; } + } + + [Fact] + public void ConvertTypes_ShouldHandle_GenericMethodArguments() + { + var ids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + Expression> filter = s => ids.Contains(s.Id); + + var mapping = new Dictionary + { + { typeof(Source), typeof(Target) } + }; + + var converted = filter.ConvertTypes(mapping); + var func = converted.Compile(); + + Assert.True(func(new Target { Id = ids[0] })); + Assert.False(func(new Target { Id = Guid.NewGuid() })); + } +} diff --git a/src/Utilities/NVs.Budget.Utilities.Expressions/ReplaceTypeVisitor.cs b/src/Utilities/NVs.Budget.Utilities.Expressions/ReplaceTypeVisitor.cs index acc74ca5..3e5b6de1 100644 --- a/src/Utilities/NVs.Budget.Utilities.Expressions/ReplaceTypeVisitor.cs +++ b/src/Utilities/NVs.Budget.Utilities.Expressions/ReplaceTypeVisitor.cs @@ -59,9 +59,32 @@ private IEnumerable GetReplacements(Type[] sourceTypes) private bool RequiresReplacement(IEnumerable types) { - var genericArgs = types.Where(t => t.IsGenericType).SelectMany(t => t.GetGenericArguments()); - return types.Intersect(_mapping.Keys).Any() - || RequiresReplacement(genericArgs); + var queue = new Queue(types); + var seen = new HashSet(); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + if (!seen.Add(current)) + { + continue; + } + + if (_mapping.ContainsKey(current)) + { + return true; + } + + if (current.IsGenericType) + { + foreach (var arg in current.GetGenericArguments()) + { + queue.Enqueue(arg); + } + } + } + + return false; } diff --git a/src/Utilities/NVs.Budget.Utilities.Json.Tests/NVs.Budget.Utilities.Json.Tests.csproj b/src/Utilities/NVs.Budget.Utilities.Json.Tests/NVs.Budget.Utilities.Json.Tests.csproj index 63e6a181..c08127a9 100644 --- a/src/Utilities/NVs.Budget.Utilities.Json.Tests/NVs.Budget.Utilities.Json.Tests.csproj +++ b/src/Utilities/NVs.Budget.Utilities.Json.Tests/NVs.Budget.Utilities.Json.Tests.csproj @@ -18,7 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Utilities/NVs.Budget.Utilities.Testing/FixtureExtensions.cs b/src/Utilities/NVs.Budget.Utilities.Testing/FixtureExtensions.cs index 5a70f33d..ec98d98e 100644 --- a/src/Utilities/NVs.Budget.Utilities.Testing/FixtureExtensions.cs +++ b/src/Utilities/NVs.Budget.Utilities.Testing/FixtureExtensions.cs @@ -41,9 +41,9 @@ public static IReadOnlyList CreateTransactions(this Fixture fixture, int c public static Fixture ResetCurrency(this Fixture fixture) => fixture.ResetNamedParameter("currency"); - public static IDisposable SetAccount(this Fixture fixture, Domain.Entities.Accounts.Budget budget) => fixture.SetNamedParameter(nameof(Operation.Budget).ToLower(), budget); + public static IDisposable SetBudget(this Fixture fixture, Domain.Entities.Budgets.Budget budget) => fixture.SetNamedParameter(nameof(Operation.Budget).ToLower(), budget); - public static Fixture ResetAccount(this Fixture fixture) => fixture.ResetNamedParameter(nameof(Operation.Budget).ToLower()); + public static Fixture ResetBudget(this Fixture fixture) => fixture.ResetNamedParameter(nameof(Operation.Budget).ToLower()); private class Scope(Fixture fixture, string name) : IDisposable { diff --git a/src/Utilities/NVs.Budget.Utilities.Yaml/NVs.Budget.Utilities.Yaml.csproj b/src/Utilities/NVs.Budget.Utilities.Yaml/NVs.Budget.Utilities.Yaml.csproj new file mode 100644 index 00000000..4c0ef8b4 --- /dev/null +++ b/src/Utilities/NVs.Budget.Utilities.Yaml/NVs.Budget.Utilities.Yaml.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/Utilities/NVs.Budget.Utilities.Yaml/UnexpectedNodeTypeError.cs b/src/Utilities/NVs.Budget.Utilities.Yaml/UnexpectedNodeTypeError.cs new file mode 100644 index 00000000..1bbacf29 --- /dev/null +++ b/src/Utilities/NVs.Budget.Utilities.Yaml/UnexpectedNodeTypeError.cs @@ -0,0 +1,11 @@ +namespace NVs.Budget.Utilities.Yaml; + +public class UnexpectedNodeTypeError : YamlParsingError +{ + public UnexpectedNodeTypeError(Type type, Type expected, ICollection path) : base("Unexpected node type found", path) + { + Metadata.Add("Key", path.LastOrDefault() ?? string.Empty); + Metadata.Add("Expected", expected.Name); + Metadata.Add("Type", type.Name); + } +} \ No newline at end of file diff --git a/src/Utilities/NVs.Budget.Utilities.Yaml/YamlParsingError.cs b/src/Utilities/NVs.Budget.Utilities.Yaml/YamlParsingError.cs new file mode 100644 index 00000000..eb523960 --- /dev/null +++ b/src/Utilities/NVs.Budget.Utilities.Yaml/YamlParsingError.cs @@ -0,0 +1,10 @@ +using FluentResults; + +namespace NVs.Budget.Utilities.Yaml; + +public class YamlParsingError(string reason, IEnumerable path) : IError +{ + public string Message { get; } = reason; + public Dictionary Metadata { get; } = new() { {"Path", string.Join('.', path) } }; + public List Reasons { get; } = new(); +} \ No newline at end of file diff --git a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlReader.cs b/src/Utilities/NVs.Budget.Utilities.Yaml/YamlReader.cs similarity index 81% rename from src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlReader.cs rename to src/Utilities/NVs.Budget.Utilities.Yaml/YamlReader.cs index ed36f5f1..f68994b3 100644 --- a/src/Infrastructure/NVs.Budget.Infrastructure.IO.Console/Input/YamlReader.cs +++ b/src/Utilities/NVs.Budget.Utilities.Yaml/YamlReader.cs @@ -1,18 +1,16 @@ using FluentResults; -using NVs.Budget.Controllers.Console.Contracts.Errors; -using NVs.Budget.Infrastructure.IO.Console.Input.Criteria.Logbook; using YamlDotNet.Core; using YamlDotNet.RepresentationModel; -namespace NVs.Budget.Infrastructure.IO.Console.Input; +namespace NVs.Budget.Utilities.Yaml; -internal abstract class YamlReader +public abstract class YamlReader { protected static readonly string[] EmptyPath = []; protected Result LoadRootNodeFrom(StreamReader reader) { var stream = new YamlStream(); - try { stream.Load(reader); } catch(YamlException e) { return Result.Fail(new ExceptionBasedError(e, e.ToString())); } + try { stream.Load(reader); } catch(YamlException e) { return Result.Fail(new ExceptionalError(e)); } var count = stream.Documents.Count; if (count != 1)