diff --git a/.sqlx/query-f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15.json b/.sqlx/query-f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15.json new file mode 100644 index 0000000..ac1345c --- /dev/null +++ b/.sqlx/query-f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15.json @@ -0,0 +1,66 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n transaction_id,\n account_id,\n bank_code,\n amount,\n currency,\n credit_debit_indicator,\n transaction_information,\n booking_date_time\n FROM transactions\n WHERE account_id IN (\n SELECT account_id FROM accounts WHERE user_id = $1\n )\n ORDER BY booking_date_time DESC\n LIMIT $2 OFFSET $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "transaction_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "account_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "bank_code", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "amount", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "currency", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "credit_debit_indicator", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "transaction_information", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "booking_date_time", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15" +} diff --git a/README.md b/README.md index 7ce4ce6..2fb920d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,385 @@ just db-logs # Логи БД docker ps # Список запущенных контейнеров ``` + +## API Documentation (для фронтендеров) + +Вот **полная документация нашего API** на основе bash скрипта: + +```markdown +# Multiberry Backend API Documentation + +## Base URL +``` +http://localhost:3000/api +``` + +## Authentication Endpoints + +### 1. Register User +**POST** `/auth/register` + +``` +{ + "bank_user_number": 6, + "password": "testpass123" +} +``` + +**Response (201 Created)** +``` +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "bank_user_id": "team275-6" +} +``` + --- -**Happy coding!** 🚀 +### 2. Login +**POST** `/auth/login` + +``` +{ + "bank_user_id": "team275-6", + "password": "testpass123" +} +``` + +**Response (200 OK)** +``` +{ + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...", + "bank_user_id": "team275-6" +} +``` + +--- + +### 3. Get Current User Info +**GET** `/auth/me` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "user_id": 1, + "bank_user_id": "team275-6" +} +``` + +--- + +## Consent Management (Open Banking Permission) + +### 4. Request Consent from Bank +**POST** `/consent/{bank}/{bank_user_id}` + +**Parameters** +- `bank`: `vbank`, `abank`, или `sbank` +- `bank_user_id`: например `team275-6` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (201 Created)** +``` +{ + "consent_id": "consent-2c4ea1d99e5b", + "expires_at": "2026-11-07T16:03:13.562301022Z", + "status": "approved", + "message": "Согласие одобрено автоматически" +} +``` + +--- + +### 5. Get Consent Status +**GET** `/consent/{bank}/{bank_user_id}` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "consent_id": "consent-2c4ea1d99e5b", + "bank": "vbank", + "user_id": "team275-6" +} +``` + +--- + +### 6. Revoke Consent +**DELETE** `/consent/{bank}/{bank_user_id}` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "status": "deleted" +} +``` + +--- + +## Account Aggregation + +### 7. Get All Accounts (from Bank via Multiberry) +**GET** `/accounts/{bank}/{bank_user_id}` + +**Parameters** +- `bank`: `vbank`, `abank`, или `sbank` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "data": { + "account": [ + { + "accountId": "acc-3848", + "status": "Enabled", + "currency": "RUB", + "accountType": "Personal", + "accountSubType": "Checking", + "nickname": "Checking счет", + "openingDate": "2024-10-30", + "description": null, + "account": [ + { + "schemeName": "RU.CBR.PAN", + "identification": "4081781027508005359", + "name": "Кредитов Кредит Кредитович (team275)" + } + ] + } + ] + }, + "links": { + "self": "/accounts" + }, + "meta": { + "totalPages": 1 + } +} +``` + +**Cached in DB** ✅ - следующий запрос вернёт из кэша + +--- + +## Balance Retrieval + +### 8. Get Account Balance (Cached) +**GET** `/balances/{bank}/{account_id}` + +**Parameters** +- `bank`: `vbank`, `abank`, или `sbank` +- `account_id`: например `acc-3848` + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "data": { + "balance": [ + { + "accountId": "acc-3848", + "type": "InterimAvailable", + "dateTime": "2025-11-07T18:14:52.937793Z", + "amount": { + "amount": "115879.84", + "currency": "RUB" + }, + "creditDebitIndicator": "Credit" + }, + { + "accountId": "acc-3848", + "type": "InterimBooked", + "dateTime": "2025-11-07T18:14:52.937809Z", + "amount": { + "amount": "115879.84", + "currency": "RUB" + }, + "creditDebitIndicator": "Credit" + } + ] + } +} +``` + +**Cached in DB** ✅ + +--- + +## Transaction History (для generative API) + +### 9. Get Transactions (Paginated + Cached) +**GET** `/transactions/{bank}/{bank_user_id}/{account_id}?page=1&limit=6` + +**Parameters** +- `bank`: `vbank`, `abank`, или `sbank` +- `bank_user_id`: например `team275-6` +- `account_id`: например `acc-3848` +- `page`: номер страницы (default: 1) +- `limit`: количество записей (default: 6) + +**Headers** +``` +Authorization: Bearer {token} +``` + +**Response (200 OK)** +``` +{ + "data": { + "transaction": [ + { + "accountId": "acc-3848", + "transactionId": "tx-team275-8-m0-1", + "amount": { + "amount": "80462.92", + "currency": "RUB" + }, + "creditDebitIndicator": "Credit", + "status": "Booked", + "bookingDateTime": "2025-10-28T17:59:45.080562Z", + "valueDateTime": "2025-10-28T17:59:45.080562Z", + "transactionInformation": "💼 Зарплата", + "bankTransactionCode": { + "code": "ReceivedCreditTransfer" + } + } + ] + }, + "links": { + "self": "/accounts/acc-3848/transactions?page=1&limit=6", + "next": "/accounts/acc-3848/transactions?page=2&limit=6" + }, + "meta": { + "totalPages": 12, + "totalRecords": 67, + "currentPage": 1, + "pageSize": 6 + } +} +``` + +**Cached in DB** ✅ - 67 транзакций сохранены! + +--- + +## Error Handling + +All endpoints return errors in format: +``` +{ + "error": "Description", + "details": "Additional info if available" +} +``` + +--- + +## Database Schema (что сохраняется) + +``` +-- Users: register/login +users (id, bank_user_id, password_hash, created_at) + +-- Consents: permissions from banks +user_consents (user_id, bank_code, consent_id, status, expires_at) + +-- Accounts: aggregated from all banks +accounts (account_id, user_id, bank_code, nickname, currency, status, etc) + +-- Balances: current balances (cached) +balances (account_id, balance_type, amount, currency, date_time) + +-- Transactions: all transactions (cached) +transactions (transaction_id, account_id, bank_code, amount, currency, etc) +``` + +--- + +## Frontend Integration (Dioxus) + +``` +// Example: Get accounts for multi-bank view +let accounts = fetch_accounts("vbank", "team275-6", token).await; +for account in accounts.data.account { + println!("{}: {}", account.nickname, account.currency); +} + +// Get transactions for chart/visualization +let txs = fetch_transactions("vbank", "team275-6", "acc-3848", 1, 6, token).await; +for tx in txs.data.transaction { + // Pass to image generation API + generate_transaction_chart(&tx).await; +} +``` + +--- + +## Multi-Bank Usage + +``` +# Get accounts from VBank +GET /accounts/vbank/team275-6 + +# Get accounts from ABank (same user, different bank) +GET /accounts/abank/team275-6 + +# Get accounts from SBank +GET /accounts/sbank/team275-6 + +# All cached! Switch between banks instantly ⚡ +``` +``` + +*** + +## Для Image Generation API + +Структура данных транзакции для передачи в generative API: + +```json +{ + "transaction": { + "accountId": "acc-3848", + "transactionId": "tx-team275-8-m0-1", + "amount": "80462.92", + "currency": "RUB", + "creditDebitIndicator": "Credit", + "transactionInformation": "💼 Зарплата", + "bookingDateTime": "2025-10-28T17:59:45.080562Z", + "status": "Booked" + }, + "prompt_template": "Transaction: {transactionInformation} for {amount} {currency} - {creditDebitIndicator}" +} +``` + +*** + diff --git a/src/route.rs b/src/route.rs index 9ac1379..c49f945 100644 --- a/src/route.rs +++ b/src/route.rs @@ -26,6 +26,9 @@ pub fn router(app_state: AppState) -> Router { .route("/api/transactions/{bank}/{user_id}/{account_id}", get(handlers::get_transactions_handler) ) + .route("/api/transactions/{bank_user_id}", + get(handlers::get_all_transactions_handler) // ← Новый эндпоинт + ) .route("/api/balances/{bank}/{account_id}", get(handlers::get_balances_handler) ) diff --git a/src/route/handlers.rs b/src/route/handlers.rs index 27957e3..8af6cc7 100644 --- a/src/route/handlers.rs +++ b/src/route/handlers.rs @@ -212,6 +212,90 @@ pub async fn get_transactions_handler( Ok(Json(serde_json::to_value(transactions_response).unwrap())) } +pub async fn get_all_transactions_handler( + State(state): State, + Path(bank_user_id): Path, + Query(params): Query, // ← Добавили query params +) -> Result, (StatusCode, Json)> { + let page = params.page.unwrap_or(1); + let limit = params.limit.unwrap_or(20); // Default: 20 на странице для ленты + + info!("📊 Fetching ALL transactions page {} (limit {})", page, limit); + + // Validate + if limit > 100 { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({ "error": "limit max 100" })) + )); + } + + let offset = (page - 1) * limit; + + // Get total count + let total_count: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM transactions WHERE account_id IN ( + SELECT account_id FROM accounts WHERE user_id = $1 + )" + ) + .bind(&bank_user_id) + .fetch_one(&state.db_pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?; + + // Get paginated transactions + let transactions = sqlx::query!( + r#" + SELECT + transaction_id, + account_id, + bank_code, + amount, + currency, + credit_debit_indicator, + transaction_information, + booking_date_time + FROM transactions + WHERE account_id IN ( + SELECT account_id FROM accounts WHERE user_id = $1 + ) + ORDER BY booking_date_time DESC + LIMIT $2 OFFSET $3 + "#, + bank_user_id, + limit as i64, + offset as i64 + ) + .fetch_all(&state.db_pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?; + + let total_pages = (total_count as f64 / limit as f64).ceil() as u32; + + Ok(Json(json!({ + "data": { + "transactions": transactions.into_iter().map(|tx| json!({ + "transaction_id": tx.transaction_id, + "account_id": tx.account_id, + "bank_code": tx.bank_code, + "amount": tx.amount, + "currency": tx.currency, + "credit_debit_indicator": tx.credit_debit_indicator, + "transaction_information": tx.transaction_information, + "booking_date_time": tx.booking_date_time, + })).collect::>() + }, + "meta": { + "current_page": page, + "page_size": limit, + "total_pages": total_pages, + "total_records": total_count, + "has_next": page < total_pages, + "has_prev": page > 1, + } + }))) +} + pub async fn delete_consent_handler(