(feat) all transactions
This commit is contained in:
parent
aeb9514aa3
commit
eef798df5f
4 changed files with 533 additions and 1 deletions
66
.sqlx/query-f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15.json
generated
Normal file
66
.sqlx/query-f95bb0ebb7f721265b971a743f09c6f2b0491616b91932b94b0ba8f22ba91d15.json
generated
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
381
README.md
381
README.md
|
|
@ -149,6 +149,385 @@ just db-logs # Логи БД
|
||||||
docker ps # Список запущенных контейнеров
|
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}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ pub fn router(app_state: AppState) -> Router {
|
||||||
.route("/api/transactions/{bank}/{user_id}/{account_id}",
|
.route("/api/transactions/{bank}/{user_id}/{account_id}",
|
||||||
get(handlers::get_transactions_handler)
|
get(handlers::get_transactions_handler)
|
||||||
)
|
)
|
||||||
|
.route("/api/transactions/{bank_user_id}",
|
||||||
|
get(handlers::get_all_transactions_handler) // ← Новый эндпоинт
|
||||||
|
)
|
||||||
.route("/api/balances/{bank}/{account_id}",
|
.route("/api/balances/{bank}/{account_id}",
|
||||||
get(handlers::get_balances_handler)
|
get(handlers::get_balances_handler)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,90 @@ pub async fn get_transactions_handler(
|
||||||
Ok(Json(serde_json::to_value(transactions_response).unwrap()))
|
Ok(Json(serde_json::to_value(transactions_response).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_transactions_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(bank_user_id): Path<String>,
|
||||||
|
Query(params): Query<TransactionQuery>, // ← Добавили query params
|
||||||
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
},
|
||||||
|
"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(
|
pub async fn delete_consent_handler(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue