{"id":49901460,"url":"https://github.com/nico-iaco/nexabudget-be","last_synced_at":"2026-05-16T06:37:20.857Z","repository":{"id":321120894,"uuid":"1083296657","full_name":"nico-iaco/nexabudget-be","owner":"nico-iaco","description":"Backend for NexaBudget, a personal finance management app. This Spring Boot application provides a RESTful API for managing finances, including accounts, transactions, and budgets.","archived":false,"fork":false,"pushed_at":"2026-05-13T16:23:03.000Z","size":546,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T17:24:53.934Z","etag":null,"topics":["gemini-ai","graalvm-native-image","java-25","mongodb-atlas","personal-finance","redis-cache","semantic-cache","spring-ai","spring-security","springboot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nico-iaco.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-25T18:22:25.000Z","updated_at":"2026-05-13T16:23:07.000Z","dependencies_parsed_at":"2025-11-05T00:02:21.035Z","dependency_job_id":null,"html_url":"https://github.com/nico-iaco/nexabudget-be","commit_stats":null,"previous_names":["nico-iaco/nexabudget-be"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nico-iaco/nexabudget-be","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nico-iaco%2Fnexabudget-be","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nico-iaco%2Fnexabudget-be/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nico-iaco%2Fnexabudget-be/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nico-iaco%2Fnexabudget-be/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nico-iaco","download_url":"https://codeload.github.com/nico-iaco/nexabudget-be/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nico-iaco%2Fnexabudget-be/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33092753,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-16T04:41:52.686Z","status":"ssl_error","status_checked_at":"2026-05-16T04:41:52.009Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gemini-ai","graalvm-native-image","java-25","mongodb-atlas","personal-finance","redis-cache","semantic-cache","spring-ai","spring-security","springboot"],"created_at":"2026-05-16T06:37:20.035Z","updated_at":"2026-05-16T06:37:20.850Z","avatar_url":"https://github.com/nico-iaco.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NexaBudget - Backend\n\nThis is the backend service for NexaBudget, a personal finance management application. It provides a RESTful API for handling users, accounts, transactions, budgets, and categories.\n\n## Built With\n\n- Java 25\n- Spring Boot 4.0.0\n- Spring Data JPA\n- Spring Security with JWT Authentication\n- Maven\n- Lombok\n- SpringDoc OpenAPI (Swagger UI)\n- PostgreSQL\n- MongoDB (for Vector Store / Semantic Cache)\n- Valkey/Redis (for caching)\n- Spring AI with Google Gemini AI (for transaction categorization)\n- Spring Boot Actuator with Prometheus\n- GraalVM Native Image support\n\n## Database Schema\n\nThe application uses a relational database to persist data. All entities use **UUID** as primary keys for better scalability and security. The main entities are:\n\n- **User**: Represents a user of the application.\n  - `id` (Primary Key, UUID)\n  - `username` (unique)\n  - `email` (unique)\n  - `password_hash`\n  - `created_at`\n  - `updated_at`\n\n- **Account**: Represents a financial account (e.g., bank account, credit card).\n  - `id` (Primary Key, UUID)\n  - `name`\n  - `type` (e.g., CASH, CHECKING, SAVINGS)\n  - `currency`\n  - `user_id` (Foreign Key to User, UUID)\n  - `requisition_id` (for GoCardless integration)\n  - `external_account_id` (for GoCardless integration)\n  - `last_external_sync`\n  - `created_at`\n  - `deleted` (soft-delete flag, default `false`)\n  - `deleted_at` (timestamp of soft deletion)\n\n- **Category**: Represents a category for transactions (e.g., Food, Salary).\n  - `id` (Primary Key, UUID)\n  - `name`\n  - `transaction_type` (INCOME or EXPENSE)\n  - `user_id` (Foreign Key to User, UUID, nullable for default categories)\n\n- **Transaction**: Represents a single financial transaction.\n  - `id` (Primary Key, UUID)\n  - `amount` (BigDecimal)\n  - `description`\n  - `transaction_date`\n  - `type` (INCOME or EXPENSE)\n  - `note` (optional text field)\n  - `user_id` (Foreign Key to User, UUID)\n  - `account_id` (Foreign Key to Account, UUID)\n  - `category_id` (Foreign Key to Category, UUID)\n  - `transfer_id` (for linked transfers)\n  - `external_id` (for GoCardless integration)\n  - `created_at`\n  - `deleted` (soft-delete flag, default `false`)\n  - `deleted_at` (timestamp of soft deletion)\n  - `exchange_rate` (nullable — populated on the IN leg of a multi-currency transfer)\n  - `original_currency` (nullable — source currency for multi-currency transfers)\n  - `original_amount` (nullable — amount in source currency for multi-currency transfers)\n  - `import_hash` (nullable — SHA-256 dedup hash for CSV/OFX imports)\n\n- **Budget**: Represents a spending or saving goal for a specific category.\n  - `id` (Primary Key, UUID)\n  - `budget_limit` (BigDecimal)\n  - `start_date`\n  - `end_date`\n  - `category_id` (Foreign Key to Category, UUID)\n  - `user_id` (Foreign Key to User, UUID)\n  - `created_at`\n\n- **CryptoHolding**: Represents a cryptocurrency asset held by the user.\n  - `id` (Primary Key, UUID)\n  - `symbol` (e.g., BTC, ETH)\n  - `amount` (BigDecimal)\n  - `source` (MANUAL or BINANCE)\n  - `user_id` (Foreign Key to User, UUID)\n\n- **UserBinanceKeys**: Stores encrypted API keys for Binance integration.\n  - `id` (Primary Key, UUID)\n  - `api_key` (Encrypted)\n  - `api_secret` (Encrypted)\n  - `user_id` (Foreign Key to User, UUID)\n\n- **BudgetTemplate**: A recurring budget definition that auto-instantiates budgets on a schedule.\n  - `id` (Primary Key, UUID)\n  - `budget_limit` (BigDecimal)\n  - `recurrence_type` (`MONTHLY`, `QUARTERLY`, `YEARLY`)\n  - `active` (default `true`)\n  - `category_id` (Foreign Key to Category, UUID)\n  - `user_id` (Foreign Key to User, UUID)\n  - `created_at`\n\n- **BudgetAlert**: A threshold-based alert for budget spending.\n  - `id` (Primary Key, UUID)\n  - `threshold_percentage` (1–100)\n  - `active` (default `true`)\n  - `last_notified_at`\n  - `budget_id` (Foreign Key to Budget, UUID)\n  - `user_id` (Foreign Key to User, UUID)\n  - `created_at`\n\n- **AuditLog**: Immutable record of every write operation on core entities.\n  - `id` (Primary Key, UUID)\n  - `user_id` (UUID of the actor)\n  - `action` (e.g. `CREATE_TRANSACTION`, `DELETE_ACCOUNT`)\n  - `entity_type` (e.g. `Transaction`, `Account`)\n  - `entity_id` (ID of the affected entity)\n  - `new_value` (JSON snapshot of response DTO, nullable)\n  - `timestamp`\n  - `ip_address` (nullable)\n\n- **ApiKey**: Machine-to-machine API keys for accessing the API without JWT.\n  - `id` (Primary Key, UUID)\n  - `name`\n  - `key_hash` (SHA-256 of the plaintext key — never stored plain)\n  - `scopes` (comma-separated, e.g. `READ_ALL,WRITE_TRANSACTIONS`)\n  - `expires_at` (nullable)\n  - `last_used_at` (nullable)\n  - `active` (default `true`)\n  - `user_id` (Foreign Key to User, UUID)\n  - `created_at`\n\n## Getting Started\n\nTo get a local copy up and running, follow these simple steps.\n\n### Prerequisites\n\n- JDK 25 or newer\n- Maven\n- Docker and Docker Compose (optional, for containerized deployment)\n- MongoDB instance (for semantic caching)\n\n### 1. Clone the repository\n\n```shell\ngit clone \u003cyour-repository-url\u003e\ncd nexaBudget-be\n```\n\n### 2. Running the Application\n\nYou can run the application either locally using Maven or with Docker.\n\n#### Running Locally with Maven\n\nThis method is ideal for development and debugging.\n\n##### 1. Database Setup\n\nYou need a running instance of PostgreSQL. Set the following environment variables or update `src/main/resources/application.properties`:\n\n```properties\n# Database Configuration\nspring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/nexabudget}\nspring.datasource.username=nexabudget-be\nspring.datasource.password=${DB_PWD:your_password}\nspring.datasource.driver-class-name=org.postgresql.Driver\n\n# JPA Configuration\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.open-in-view=false\n\n# JWT Configuration\napp.jwtSecret=${JWT_SECRET:tua-chiave-segreta-molto-lunga-e-sicura-di-almeno-64-caratteri}\napp.jwtExpirationInMs=86400000\n\n# Crypto Encryption (for API Keys)\ncrypto.encryption.key=${CRYPTO_ENCRYPTION_KEY:MySuperSecretKey1234567890123456}\n\n# GoCardless Integration\ngocardless.integrator.baseUrl=http://localhost:3000\n\n# Google Gemini AI Configuration\nspring.ai.google.genai.api-key=${GEMINI_API_KEY:your_gemini_api_key}\nspring.ai.google.genai.chat.options.model=gemini-2.5-flash-lite\nspring.ai.google.genai.chat.options.temperature=0.1\n\n# Semantic Cache / Vector Store (MongoDB)\nspring.mongodb.uri=${MONGODB_URI:mongodb://localhost:27017/nexabudget-be}\nspring.ai.vectorstore.mongodb.collection-name=semantic_cache\nspring.ai.vectorstore.mongodb.index-name=semantic_cache_index\nspring.ai.vectorstore.mongodb.initialize-schema=false\n\n# Redis/Valkey Cache Configuration\nspring.cache.type=redis\nspring.data.redis.host=${REDIS_HOST:localhost}\nspring.data.redis.port=${REDIS_PORT:6379}\nspring.data.redis.password=${REDIS_PASSWORD:}\nspring.data.redis.username=${REDIS_USERNAME:}\nspring.data.redis.database=0\nspring.data.redis.ssl.enabled=${REDIS_SSL_ENABLED:false}\n\n# Actuator and Monitoring\nmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus\nmanagement.endpoint.health.show-details=always\n```\n\n**Required Environment Variables:**\n\n- `DB_URL`: PostgreSQL database URL\n- `DB_PWD`: PostgreSQL database password\n- `JWT_SECRET`: Secret key for JWT signing — **must be set and must not equal the dev default**; the application refuses to start otherwise (min 32 chars)\n- `GEMINI_API_KEY`: Google Gemini API key\n- `MONGODB_URI`: MongoDB connection URI (for vector store)\n- `CRYPTO_ENCRYPTION_KEY`: 32-char key for encrypting sensitive data (Binance keys)\n\n**Optional Environment Variables:**\n\n- `REDIS_HOST`: Redis/Valkey host (default: localhost)\n- `REDIS_PORT`: Redis/Valkey port (default: 6379)\n- `REDIS_USERNAME`: Redis/Valkey username\n- `REDIS_PASSWORD`: Redis/Valkey password\n- `REDIS_SSL_ENABLED`: Enable SSL for Redis connection (default: false)\n\n##### 2. Build the project\n\nUse Maven to build the project and download dependencies.\n\n```shell\n./mvnw clean install\n```\n\n##### 3. Run the application\n\nYou can run the application using the Spring Boot Maven plugin.\n\n```shell\n./mvnw spring-boot:run\n```\n\nThe application will start on \u003chttp://localhost:8080\u003e.\n\n#### Running with Docker\n\nThis is the recommended way to run the application in a production-like environment. The following commands will start\nthe application, PostgreSQL database, Valkey cache, and MongoDB.\n\n##### 1. Run the JVM-based image\n\nThis command builds the JVM image and starts all services (app, PostgreSQL, Valkey, MongoDB) in detached mode.\n\n```shell\ndocker-compose up --build -d\n```\n\n##### 2. Run the Native image\n\nFor better performance and a smaller memory footprint, you can run the native-compiled version.\n\n```shell\ndocker-compose -f docker-compose.native.yml up --build -d\n```\n\nIn both cases, the application will be available at \u003chttp://localhost:8080\u003e, PostgreSQL at port 5432, Valkey at port\n6379, and MongoDB at port 27017.\n\nTo stop and remove the containers, use:\n\n```shell\n# For JVM\ndocker-compose down\n\n# For Native\ndocker-compose -f docker-compose.native.yml down\n```\n\n## API Documentation\n\nOnce the application is running, you can access the Swagger UI documentation at:\n\n- **Swagger UI**: \u003chttp://localhost:8080/swagger-ui.html\u003e\n- **OpenAPI JSON**: \u003chttp://localhost:8080/v3/api-docs\u003e\n\nThe API provides the following endpoints:\n\n### Authentication (Public)\n\n- `POST /api/auth/login` - User login\n- `POST /api/auth/register` - User registration\n\n### Users (Protected)\n\n- `GET /api/users/me` - Get current user profile\n- `PUT /api/users/` - Update current user profile (partial update - only provided fields are changed)\n\n### Accounts (Protected)\n\n- `GET /api/accounts` - Get all user accounts\n- `POST /api/accounts` - Create new account\n- `GET /api/accounts/{id}` - Get account details\n- `PUT /api/accounts/{id}` - Update account\n- `DELETE /api/accounts/{id}` - Delete account\n- `GET /api/accounts/total-balance/preferred` - Get total balance of all accounts, converted to the user's preferred currency\n\n### Transactions (Protected)\n\n- `GET /api/transactions` - Get all user transactions (full list)\n- `GET /api/transactions/paged?page=0\u0026size=20` - Get transactions with pagination, sorted by date descending\n- `POST /api/transactions` - Create new transaction\n- `GET /api/transactions/{id}` - Get transaction details\n- `PUT /api/transactions/{id}` - Update transaction\n- `DELETE /api/transactions/{id}` - Delete transaction\n- `POST /api/transactions/convert-to-transfer` - Convert two existing transactions into a transfer\n- `POST /api/transactions/convert-single-to-transfer` - Create a transfer from a single existing transaction\n- `GET /api/transactions/daterange?start=\u0026end=` - Transactions in a date range\n- `GET /api/transactions/account/{id}/daterange` - Account transactions in a date range\n\n### Categories (Protected)\n\n- `GET /api/categories` - Get all categories (default + user custom)\n- `POST /api/categories` - Create custom category — returns `409 Conflict` if a category with the same name and type already exists for the user\n- `PUT /api/categories/{id}` - Update category\n- `DELETE /api/categories/{id}` - Delete category\n- `POST /api/categories/{sourceId}/merge-into/{targetId}` - Merge source into target (moves all transactions and budgets, deletes source) — returns `400` if the two categories have different transaction types\n\n### Budgets (Protected)\n\n- `GET /api/budgets/` - Get all user budgets\n- `POST /api/budgets` - Create new budget — `endDate` must be ≥ `startDate` (returns `400` otherwise)\n- `GET /api/budgets/active` - Active budgets at a given date\n- `GET /api/budgets/usage` - Spending percentage for active budgets\n- `GET /api/budgets/remaining` - Remaining budget for active budgets\n- `PUT /api/budgets/{id}` - Update budget (ownership verified — returns `404` if budget belongs to another user)\n- `DELETE /api/budgets/{id}` - Delete budget (ownership verified — returns `404` if budget belongs to another user)\n\n### GoCardless Integration (Protected)\n\n- `GET /api/gocardless/bank` - List supported banks by country\n- `POST /api/gocardless/bank/link` - Generate bank link — returns `503` if GoCardless is unavailable\n- `GET /api/gocardless/bank/{localAccountId}/account` - List bank accounts linked via GoCardless\n- `POST /api/gocardless/bank/{localAccountId}/link` - Link a bank account to a local account\n- `POST /api/gocardless/bank/{localAccountId}/sync` - Start async transaction sync (uses atomic DB lock to prevent concurrent syncs)\n\n### Crypto Portfolio (Protected)\n\n- `GET /api/crypto/portfolio` - Get crypto portfolio value (supports currency conversion)\n- `POST /api/crypto/holdings` - Add/Update manual crypto holding\n- `POST /api/crypto/binance/keys` - Save Binance API keys (encrypted)\n- `POST /api/crypto/binance/sync` - Trigger sync from Binance\n\n### Trash / Recovery (Protected)\n\nDeleted accounts and transactions are soft-deleted and retained for 30 days before permanent purge (runs daily at 3 AM).\n\n- `GET /api/trash/transactions` - List soft-deleted transactions\n- `POST /api/trash/transactions/{id}/restore` - Restore a soft-deleted transaction\n- `GET /api/trash/accounts` - List soft-deleted accounts\n- `POST /api/trash/accounts/{id}/restore` - Restore a soft-deleted account (also restores all its transactions)\n\n### Financial Reports (Protected)\n\n- `GET /api/reports/monthly-trend?months=12` - Monthly income/expense totals for the last N months\n- `GET /api/reports/category-breakdown?type=OUT\u0026startDate=\u0026endDate=` - Spending/income by category in a date range\n- `GET /api/reports/month-comparison?year=\u0026month=` - Compare a month vs. the previous month\n- `GET /api/reports/monthly-projection` - Projected end-of-month totals based on current spending rate\n- `POST /api/reports/ai-analysis` - Start an asynchronous AI financial analysis job for a given date range (returns `jobId`)\n- `GET /api/reports/ai-analysis/{jobId}` - Check the status and retrieve the result of an AI analysis job\n\n### Budget Templates (Protected)\n\nTemplates automatically instantiate budgets on the 1st of every month (MONTHLY), at the start of each quarter (QUARTERLY), and on January 1st (YEARLY).\n\n- `GET /api/budget-templates` - List all budget templates\n- `POST /api/budget-templates` - Create a budget template\n- `GET /api/budget-templates/{id}` - Get a budget template\n- `PUT /api/budget-templates/{id}` - Update a budget template\n- `DELETE /api/budget-templates/{id}` - Delete a budget template\n\n### Budget Alerts (Protected)\n\nAlerts are evaluated hourly. A notification is logged when `spent / limit ≥ thresholdPercentage` and the alert has not been triggered in the past 24 hours.\n\n- `GET /api/budget-alerts` - List all budget alerts\n- `POST /api/budget-alerts` - Create a budget alert (`thresholdPercentage`: 1–100)\n- `GET /api/budget-alerts/{id}` - Get a budget alert\n- `PUT /api/budget-alerts/{id}` - Update a budget alert\n- `DELETE /api/budget-alerts/{id}` - Delete a budget alert\n\n### Audit Log (Protected)\n\nEvery write operation on core entities (Transaction, Account, Budget, Category) is automatically recorded.\n\n- `GET /api/audit-log?page=0\u0026size=20` - Paginated audit log for the current user\n- `GET /api/audit-log/{entityType}/{entityId}` - History of a specific entity\n\n### API Keys (Protected)\n\nMachine-to-machine access without JWT. The plaintext key is shown **only once** at creation.\n\n- `POST /api/api-keys` - Create a new API key (returns plaintext key once)\n- `GET /api/api-keys` - List all API keys (no plaintext values)\n- `PUT /api/api-keys/{id}` - Update name, scopes, expiry, or active status\n- `DELETE /api/api-keys/{id}` - Delete a key permanently\n\n### Import Transactions (Protected)\n\nTwo-step import: preview first, then confirm.\n\n- `POST /api/accounts/{id}/import/csv/preview` - Preview CSV import (`multipart/form-data`: `file` + `mapping`)\n- `POST /api/accounts/{id}/import/csv` - Import from CSV\n- `POST /api/accounts/{id}/import/ofx/preview` - Preview OFX/QFX import (`multipart/form-data`: `file`)\n- `POST /api/accounts/{id}/import/ofx` - Import from OFX/QFX\n\nAll protected endpoints require a valid JWT token **or** an API key:\n\n```http\nAuthorization: Bearer \u003cyour_jwt_token\u003e\n# or\nX-Api-Key: \u003cyour_api_key\u003e\n```\n\n## Features\n\n### JWT Authentication\n\nThe application uses JWT (JSON Web Tokens) for secure authentication. Tokens expire after 24 hours by default (configurable via `app.jwtExpirationInMs` property).\n\nThe application validates the `JWT_SECRET` at startup — it will **refuse to start** if the secret is the development default or shorter than 32 characters.\n\n### Security \u0026 Validation Rules\n\n- **Password updates** are always BCrypt-encoded through `UserService.updateUserProfile()` — raw passwords are never persisted.\n- **Budget ownership** is enforced on `PUT /api/budgets/{id}` and `DELETE /api/budgets/{id}` — a user can only modify their own budgets.\n- **Category uniqueness**: a user cannot have two categories with the same name and transaction type (`UNIQUE(user_id, name, transaction_type)`).\n- **Budget date validation**: `endDate`, when provided, must be ≥ `startDate`. The API returns `400 Bad Request` otherwise.\n- **GoCardless sync**: concurrent sync attempts on the same account are prevented with an atomic DB-level lock (`UPDATE … WHERE isSynchronizing = false`).\n- **GoCardless token generation**: returns `503 Service Unavailable` (instead of a `NullPointerException`) when GoCardless does not return a valid response.\n- **Category merge**: `POST /categories/{sourceId}/merge-into/{targetId}` moves all transactions and budgets from source to target atomically via bulk JPQL UPDATE, then deletes the source. Only user-owned categories can be used as source (default categories are protected).\n\n### Resilience \u0026 Performance\n\n- **Automatic retry** on transient network failures: GoCardless API calls and exchange rate lookups retry up to 3 times with exponential backoff (1 s, 2 s) via Spring Retry `@Retryable`. Empty fallback results are never cached (via `unless` conditions), so subsequent requests retry properly.\n- **HTTP timeouts**: GoCardless RestClient: 5 s connect / 10 s read. Binance and ExchangeRate RestClients: 5 s / 5 s.\n- **Batch crypto prices**: `CryptoPortfolioService.getPortfolioValue()` fetches all USDT prices in a single `GET /api/v3/ticker/price` call instead of one call per symbol. Falls back to per-symbol lookup for any symbol not present in the batch response.\n- **Cache warming**: at startup, `CacheWarmupRunner` pre-populates the exchange rate cache (USD → EUR, USD → GBP) asynchronously so the first real request hits a warm cache.\n\n### AI-Powered Transaction Categorization\n\nTransactions can be automatically categorized using Google's Gemini AI. The AI analyzes transaction descriptions and suggests the most appropriate category based on your existing categories.\n\n### GoCardless Integration\n\nConnect your real bank accounts through GoCardless (formerly Nordigen) to automatically import transactions. The integration supports:\n\n- Creating bank connection requisitions\n- Linking external accounts\n- Syncing transactions from linked accounts\n\n### Crypto Portfolio Management\n\nTrack your cryptocurrency assets in one place:\n\n- **Binance Integration**: securely connect your Binance account to auto-sync holdings.\n- **Manual Tracking**: add assets from other wallets or exchanges manually.\n- **Portfolio Valuation**: view your total portfolio balance in your preferred currency.\n\n### Soft Delete \u0026 Trash Recovery\n\nDeleting a transaction or account performs a **soft delete** — the record is marked `deleted = true` with a timestamp instead of being removed from the database. All normal JPA queries automatically filter out deleted rows via Hibernate's `@SQLRestriction(\"deleted = false\")`.\n\n- Deleted items are accessible via the `/api/trash/` endpoints for 30 days.\n- Restoring an account also restores all of its soft-deleted transactions.\n- A scheduled job at **3 AM daily** permanently purges items older than 30 days.\n\n### Financial Reports\n\nAggregate reports built directly on the transaction data:\n\n- **Monthly trend**: totals per month for income and expenses.\n- **Category breakdown**: ranked spending/income by category for any date range.\n- **Month comparison**: compare totals for any month vs. the previous one.\n- **Monthly projection**: extrapolates end-of-month totals from the current daily spending rate.\n- **AI Asynchronous Analysis**: users can generate a deep analysis report of their transactions over a requested period using the `AiReportService`. The transactions are formatted as a CSV file and passed to Google Gemini AI as a multipart file attachment (Spring AI Media) instead of plain text, avoiding context prompt bloating. The endpoint returns a pending job ID that can be polled for the final markdown report.\n\n### Budget Templates\n\nDefine recurring budget templates (`MONTHLY`, `QUARTERLY`, `YEARLY`) that automatically instantiate real budgets on a schedule:\n\n- Monthly templates fire on the **1st of every month**.\n- Quarterly templates fire on the **1st of January, April, July, October**.\n- Yearly templates fire on **January 1st**.\n\nCreating or updating a template **during the month** immediately creates or updates the current period's budget with `startDate` set to the **1st of the current month**, so all transactions from the start of the month are included in threshold calculations.\n\n### Budget Alerts\n\nSet percentage-based thresholds on any budget. An hourly scheduler checks all active alerts and sends an HTML email notification when `spent / limit ≥ thresholdPercentage`. Each alert fires **at most once per budget period**: a notification is suppressed if `lastNotifiedAt ≥ budget.startDate`, so the alert resets automatically when the next period's budget is created.\n\n### Multi-Currency Transfers\n\nWhen a transfer is created between two accounts with different currencies, the application automatically fetches the current exchange rate via `ExchangeRateService` and converts the destination amount. The IN-leg transaction stores `exchangeRate`, `originalCurrency`, and `originalAmount` for full auditability.\n\n### Audit Log\n\nEvery write operation (create, update, delete) on Transactions, Accounts, Budgets, and Categories is automatically recorded via a Spring AOP aspect (`AuditAspect`) — no changes to service code required. Each entry captures the user ID, action name, affected entity type/ID, serialized response DTO, timestamp, and client IP address.\n\n### API Key Management\n\nUsers can generate API keys for machine-to-machine access (e.g. scripts, dashboards, automations). Keys are:\n\n- Generated with a cryptographically secure random 32-byte value (base64url-encoded)\n- Stored only as a SHA-256 hash — the plaintext is shown exactly once at creation\n- Validated on every request via `ApiKeyAuthenticationFilter` (reads `X-Api-Key` header)\n- Configurable with scopes, expiry dates, and active/inactive status\n\n### Import Transactions (CSV / OFX)\n\nImport transactions from bank exports in CSV or OFX format:\n\n- **CSV**: flexible column mapping (date column, amount column, description column, optional type column, date format, delimiter)\n- **OFX**: supports both OFX 1.x SGML and OFX 2.x XML formats\n- **Two-step flow**: call `/preview` first to see which rows are duplicates, then `/import` to confirm\n- **Deduplication**: SHA-256 hash of `(accountId | date | amount | description)` stored in `import_hash`; OFX FITID tracked via `external_id`\n- **AI auto-categorization**: each imported transaction is automatically categorized via Google Gemini\n\n### Semantic Caching (MongoDB Vector Store)\n\nUses **Spring AI** with **MongoDB Atlas** as a vector store to semantically cache AI responses, reducing costs and latency for repeated or similar queries.\n\n### Caching with Redisson \u0026 Valkey/Redis\n\nThe application uses **Redisson**, an advanced Redis client, to connect to Valkey (a Redis-compatible cache) to improve\nperformance and reduce external API calls.\n\n**Why Redisson?**\n\n- 🚀 Superior performance with optimized connection pooling\n- 🔧 Advanced features: distributed locks, collections, pub/sub\n- 🎯 Better error handling with automatic retries and failover\n- 📦 Native JSON codec using Jackson\n\n**Configuration Approach:**\n\n- ✅ Programmatic configuration via `RedissonConfig.java`\n- ✅ Type-safe and compile-time verified\n- ✅ Automatic SSL support for production\n- ✅ No external YAML files needed\n\n#### Cached Methods (6-hour TTL)\n\n- **`getBankAccounts`**: Caches bank account lists from GoCardless\n  - Cache key: `requisitionId`\n  - Duration: 6 hours\n- **`getGoCardlessTransaction`**: Caches transactions from GoCardless\n  - Cache key: `requisitionId_accountId`\n  - Duration: 6 hours\n\n#### Benefits\n\n- 🚀 Faster response times for repeated queries\n- 💰 Reduced API calls to external services\n- ⚡ Better scalability under load\n- 🔄 Automatic retry and reconnection handling\n\nFor more details, see [CACHE.md](./CACHE.md).\n\n### Monitoring and Health Checks\n\nThe application exposes Spring Boot Actuator endpoints for monitoring:\n\n- **Health**: \u003chttp://localhost:8080/actuator/health\u003e\n- **Metrics**: \u003chttp://localhost:8080/actuator/metrics\u003e\n- **Prometheus**: \u003chttp://localhost:8080/actuator/prometheus\u003e\n\n### GraalVM Native Image\n\nThe application supports compilation to a native executable using GraalVM, providing:\n\n- Faster startup time\n- Lower memory footprint\n- Better resource efficiency\n\n## Environment Variables\n\n| Variable                        | Description                                 | Required | Default                                                        |\n|---------------------------------|---------------------------------------------|----------|----------------------------------------------------------------|\n| `DB_URL`                        | PostgreSQL database URL                     | Yes      | jdbc:postgresql://localhost:5432/nexabudget                    |\n| `DB_PWD`                        | PostgreSQL password                         | Yes      | -                                                              |\n| `GEMINI_API_KEY`                | Google Gemini API key                       | Yes      | -                                                              |\n| `MONGODB_URI`                   | MongoDB Connection URI                      | Yes      | mongodb://localhost:27017/nexabudget-be                        |\n| `CRYPTO_ENCRYPTION_KEY`         | Key for encrypting API keys (32 chars)      | Yes      | -                                                              |\n| `REDIS_HOST`                    | Redis/Valkey host for caching               | No       | localhost                                                      |\n| `REDIS_PORT`                    | Redis/Valkey port                           | No       | 6379                                                           |\n| `REDIS_USERNAME`                | Redis/Valkey username                       | No       | (empty)                                                        |\n| `REDIS_PASSWORD`                | Redis/Valkey password                       | No       | (empty)                                                        |\n| `REDIS_SSL_ENABLED`             | Enable SSL for Redis                        | No       | false                                                          |\n| `JWT_SECRET`                    | JWT signing secret (min 32 chars, must not equal dev default) | **Yes** | — (app fails to start without it) |\n| `app.jwtExpirationInMs`         | JWT token expiration time in milliseconds   | No       | 86400000 (24 hours)                                            |\n| `gocardless.integrator.baseUrl` | GoCardless integrator service URL           | No       | \u003chttp://localhost:3000\u003e                                          |\n| `SMTP_HOST`                     | SMTP server hostname                        | No       | localhost (Mailhog in dev)                                     |\n| `SMTP_PORT`                     | SMTP server port                            | No       | 1025 (Mailhog in dev)                                          |\n| `SMTP_USER`                     | SMTP username                               | Yes (prod) | -                                                            |\n| `SMTP_PWD`                      | SMTP password                               | Yes (prod) | -                                                            |\n| `SMTP_AUTH`                     | Enable SMTP authentication                  | No       | false (set to `true` for Gmail/real SMTP)                      |\n| `SMTP_STARTTLS`                 | Enable STARTTLS                             | No       | false (set to `true` for Gmail/real SMTP)                      |\n| `MAIL_FROM`                     | Sender address for email notifications      | No       | \u003cnoreply@nexabudget.it\u003e                                          |\n\n## Development\n\n### Project Structure\n\n```text\nsrc/main/java/it/iacovelli/nexabudgetbe/\n├── config/          # Configuration classes\n├── controller/      # REST controllers\n├── dto/            # Data Transfer Objects\n├── model/          # JPA entities\n├── repository/     # Spring Data repositories\n├── security/       # JWT security components\n└── service/        # Business logic services\n```\n\n### Building Native Image\n\nTo build a native executable locally:\n\n```shell\n./mvnw -Pnative clean package\n```\n\nNote: This requires GraalVM to be installed and configured.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnico-iaco%2Fnexabudget-be","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnico-iaco%2Fnexabudget-be","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnico-iaco%2Fnexabudget-be/lists"}