(feat) transactions

This commit is contained in:
Rorik Star Platinum 2025-11-07 19:08:13 +03:00
parent 35adb141ab
commit 779ae4d498
8 changed files with 214 additions and 5 deletions

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM user_consents WHERE consent_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "2aeca288c85e977c8515a4231fddc9944bac2071c8ffa37973f1d4bd723f224e"
}

View file

@ -36,6 +36,9 @@ db-reset:
@docker compose down -v @docker compose down -v
@{{sops_run}} 'docker compose up -d' @{{sops_run}} 'docker compose up -d'
psql-exec:
@{{sops_run}} 'psql $DATABASE_URL'
stop: stop:
@echo "🛑 Stopping all services..." @echo "🛑 Stopping all services..."
@docker compose down @docker compose down

View file

@ -1,7 +1,7 @@
// src/api/accounts.rs // src/api/accounts.rs
// Account data retrieval // Account data retrieval
use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData}}; use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData, TransactionData}};
impl BankClient { impl BankClient {
pub async fn get_accounts( pub async fn get_accounts(
@ -28,4 +28,5 @@ impl BankClient {
}), }),
} }
} }
} }

View file

@ -62,4 +62,32 @@ impl BankClient {
consent consent
}) })
} }
pub async fn delete_consent(&self, consent_id: &str) -> Result<(), BankingError> {
info!("🗑️ Deleting consent: {}", consent_id);
let token = self.get_token().await?;
let response = self.http_client
.delete(self.base_url.join(&format!("/account-consents/{}", consent_id))?)
.bearer_auth(token)
.header("x-fapi-interaction-id", format!("team275-{}", chrono::Utc::now().timestamp()))
.send()
.await?;
match response.status().as_u16() {
204 => {
info!("✅ Consent deleted successfully");
Ok(())
},
status => {
error!("❌ Failed to delete consent: {}", status);
Err(BankingError::ApiError {
status,
body: response.text().await.unwrap_or_default(),
})
}
}
}
} }

View file

@ -3,6 +3,7 @@
use sqlx::PgPool; use sqlx::PgPool;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tracing::{info};
pub struct StoredConsent { pub struct StoredConsent {
pub user_id: String, pub user_id: String,
@ -55,3 +56,20 @@ pub async fn get_valid_consent(
Ok(result.map(|r| r.consent_id)) Ok(result.map(|r| r.consent_id))
} }
pub async fn delete_consent(
pool: &PgPool,
consent_id: &str,
) -> Result<(), sqlx::Error> {
info!("🗑️ Deleting consent from DB: {}", consent_id);
sqlx::query!(
"DELETE FROM user_consents WHERE consent_id = $1",
consent_id
)
.execute(pool)
.await?;
info!("✅ Consent deleted from DB");
Ok(())
}

View file

@ -18,6 +18,7 @@ pub fn router(app_state: AppState) -> Router {
.route("/api/consent/{bank}/{user_id}", .route("/api/consent/{bank}/{user_id}",
post(handlers::create_consent_handler) post(handlers::create_consent_handler)
.get(handlers::get_consent_handler) .get(handlers::get_consent_handler)
.delete(handlers::delete_consent_handler)
) )
.route("/api/accounts/{bank}/{user_id}", .route("/api/accounts/{bank}/{user_id}",
get(handlers::get_accounts_handler) get(handlers::get_accounts_handler)

View file

@ -83,7 +83,7 @@ pub async fn create_consent_handler(
StatusCode::CREATED, StatusCode::CREATED,
Json(json!({ Json(json!({
"consent_id": consent_response.consent_id, "consent_id": consent_response.consent_id,
"expires_at": consent_response.created_at + &chrono::Duration::days(365).to_string(), "expires_at": expires_at,
"status": consent_response.status, "status": consent_response.status,
"message": consent_response.message, "message": consent_response.message,
})) }))
@ -134,12 +134,31 @@ pub async fn get_accounts_handler(
let client = state.banking_clients.get_client(bank); let client = state.banking_clients.get_client(bank);
client.get_accounts(&user_id, &consent_id) let accounts_response = client.get_accounts(&user_id, &consent_id)
.await .await
.map(|accounts| Json(serde_json::to_value(accounts).unwrap())) .map_err(map_banking_error)?;
.map_err(map_banking_error)
// ✨ NEW: Save accounts to database
for account in &accounts_response.data.account {
let _ = db::accounts::store_account(
&state.db_pool,
&account.account_id,
&user_id,
bank.code(),
account.status.as_deref().unwrap_or("unknown"),
&account.currency,
&account.account_type,
account.account_sub_type.as_deref(),
&account.nickname,
account.description.as_deref(),
account.opening_date,
).await;
} }
Ok(Json(serde_json::to_value(accounts_response).unwrap()))
}
// --- Transaction Management --- // --- Transaction Management ---
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -172,6 +191,35 @@ pub async fn get_transactions_handler(
.map_err(map_banking_error) .map_err(map_banking_error)
} }
pub async fn delete_consent_handler(
State(state): State<AppState>,
Path((bank_code, user_id)): Path<(String, String)>,
) -> impl IntoResponse {
let bank = bank_code.parse::<Bank>()
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
let consent_id = db::consents::get_valid_consent(&state.db_pool, &user_id, bank.code())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
.ok_or_else(|| (StatusCode::NOT_FOUND, Json(json!({ "error": "Consent not found" }))))?;
let client = state.banking_clients.get_client(bank);
// Delete from bank first
client.delete_consent(&consent_id)
.await
.map_err(map_banking_error)?;
// Then delete from our DB
db::consents::delete_consent(&state.db_pool, &consent_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?;
Ok::<_, (StatusCode, Json<serde_json::Value>)>((StatusCode::OK, Json(json!({ "status": "deleted" }))))
}
// --- Error Mapping --- // --- Error Mapping ---
fn map_banking_error(err: BankingError) -> (StatusCode, Json<serde_json::Value>) { fn map_banking_error(err: BankingError) -> (StatusCode, Json<serde_json::Value>) {

96
test_multiberry.sh Normal file
View file

@ -0,0 +1,96 @@
#!/bin/bash
# Save as test_multiberry.sh
BASE_URL="http://localhost:3000/api"
echo "=========================================="
echo "1⃣ REGISTER USER"
echo "=========================================="
REGISTER=$(curl -s -X POST $BASE_URL/auth/register \
-H 'Content-Type: application/json' \
-d '{"bank_user_number": 8, "password": "testpass123"}')
echo "$REGISTER" | jq .
TOKEN=$(echo "$REGISTER" | jq -r '.token')
BANK_USER_ID=$(echo "$REGISTER" | jq -r '.bank_user_id')
echo "✅ Token: ${TOKEN:0:50}..."
echo "✅ Bank User ID: $BANK_USER_ID"
echo ""
echo "=========================================="
echo "2⃣ LOGIN (verify token works)"
echo "=========================================="
LOGIN=$(curl -s -X POST $BASE_URL/auth/login \
-H 'Content-Type: application/json' \
-d "{\"bank_user_id\": \"$BANK_USER_ID\", \"password\": \"testpass123\"}")
echo "$LOGIN" | jq .
echo ""
echo "=========================================="
echo "3⃣ GET ME (verify auth middleware)"
echo "=========================================="
curl -s http://localhost:3000/api/auth/me \
-H "Authorization: Bearer $TOKEN" | jq .
echo ""
echo "=========================================="
echo "4⃣ REQUEST CONSENT from VBank"
echo "=========================================="
CONSENT=$(curl -s -X POST $BASE_URL/consent/vbank/$BANK_USER_ID \
-H "Authorization: Bearer $TOKEN")
echo "$CONSENT" | jq .
CONSENT_ID=$(echo "$CONSENT" | jq -r '.consent_id')
echo "✅ Consent ID: $CONSENT_ID"
echo ""
echo "=========================================="
echo "5⃣ GET ACCOUNTS (auto-saved to DB)"
echo "=========================================="
ACCOUNTS=$(curl -s $BASE_URL/accounts/vbank/$BANK_USER_ID \
-H "Authorization: Bearer $TOKEN")
echo "$ACCOUNTS" | jq .
ACCOUNT_ID=$(echo "$ACCOUNTS" | jq -r '.data.account[0].accountId')
echo "✅ Account ID: $ACCOUNT_ID"
echo ""
echo "=========================================="
echo "6⃣ GET BALANCES"
echo "=========================================="
curl -s $BASE_URL/balances/vbank/$BANK_USER_ID \
-H "Authorization: Bearer $TOKEN" | jq .
echo ""
echo "=========================================="
echo "7⃣ GET TRANSACTIONS (page 1, limit 6)"
echo "=========================================="
curl -s "$BASE_URL/transactions/vbank/$BANK_USER_ID/$ACCOUNT_ID?page=1&limit=6" \
-H "Authorization: Bearer $TOKEN" | jq .
echo ""
echo "=========================================="
echo "8⃣ GET TRANSACTIONS (page 2, limit 6)"
echo "=========================================="
curl -s "$BASE_URL/transactions/vbank/$BANK_USER_ID/$ACCOUNT_ID?page=2&limit=6" \
-H "Authorization: Bearer $TOKEN" | jq .
echo ""
echo "=========================================="
echo "9⃣ DELETE CONSENT"
echo "=========================================="
curl -s -X DELETE $BASE_URL/consent/vbank/$BANK_USER_ID \
-H "Authorization: Bearer $TOKEN" | jq .
echo ""
echo "=========================================="
echo "🔟 VERIFY DB (from another terminal)"
echo "=========================================="
echo "just psql-exec"
echo "SELECT * FROM users;"
echo "SELECT * FROM user_consents;"
echo "SELECT * FROM accounts;"
echo "SELECT * FROM transactions;"