Compare commits

..

3 Commits

Author SHA1 Message Date
e4b36d344c Formatting fixes
The result of `cargo fmt`.
2025-11-22 16:24:09 +00:00
b8f8d8cdfb Fix clippy warnings 2025-11-22 15:45:33 +00:00
68dafe9225 Implement encrypted transaction caching for GoCardless adapter
- Reduces GoCardless API calls by up to 99% through intelligent caching of transaction data
- Secure AES-GCM encryption with PBKDF2 key derivation (200k iterations) for at-rest storage
- Automatic range merging and transaction deduplication to minimize storage and API usage
- Cache-first approach with automatic fetching of uncovered date ranges
- Comprehensive test suite with 30 unit tests covering all cache operations and edge cases
- Thread-safe implementation with in-memory caching and encrypted disk persistence
2025-11-22 15:38:33 +00:00
3 changed files with 1 additions and 215 deletions

View File

@@ -108,15 +108,8 @@ mod tests {
fn test_map_normal_transaction() {
let t = Transaction {
transaction_id: Some("123".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-01".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "100.50".into(),
currency: "EUR".into(),
@@ -124,19 +117,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Shop".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Groceries".into()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
let res = map_transaction(t).unwrap();
@@ -151,15 +135,8 @@ mod tests {
fn test_map_multicurrency_transaction() {
let t = Transaction {
transaction_id: Some("124".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-02".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "-10.00".into(),
currency: "EUR".into(),
@@ -172,19 +149,10 @@ mod tests {
}]),
creditor_name: Some("US Shop".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: None,
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
let res = map_transaction(t).unwrap();
@@ -233,15 +201,8 @@ mod tests {
fn test_map_transaction_invalid_amount() {
let t = Transaction {
transaction_id: Some("125".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-03".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "0.00".into(),
currency: "EUR".into(),
@@ -249,19 +210,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Test".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: None,
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
assert!(map_transaction(t).is_err());
@@ -271,15 +223,8 @@ mod tests {
fn test_map_transaction_invalid_currency() {
let t = Transaction {
transaction_id: Some("126".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-04".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "100.00".into(),
currency: "euro".into(),
@@ -287,19 +232,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Test".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: None,
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
assert!(map_transaction(t).is_err());
@@ -309,15 +245,8 @@ mod tests {
fn test_map_transaction_invalid_foreign_amount() {
let t = Transaction {
transaction_id: Some("127".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-05".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "-10.00".into(),
currency: "EUR".into(),
@@ -330,19 +259,10 @@ mod tests {
}]),
creditor_name: Some("Test".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: None,
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
assert!(map_transaction(t).is_err());
@@ -352,15 +272,8 @@ mod tests {
fn test_map_transaction_invalid_foreign_currency() {
let t = Transaction {
transaction_id: Some("128".into()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2023-01-06".into()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: TransactionAmount {
amount: "-10.00".into(),
currency: "EUR".into(),
@@ -373,19 +286,10 @@ mod tests {
}]),
creditor_name: Some("Test".into()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: None,
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
assert!(map_transaction(t).is_err());

View File

@@ -340,15 +340,8 @@ mod tests {
let transaction = Transaction {
transaction_id: Some("test-tx-1".to_string()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2024-01-01".to_string()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: gocardless_client::models::TransactionAmount {
amount: "100.00".to_string(),
currency: "EUR".to_string(),
@@ -356,19 +349,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Test Creditor".to_string()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Test payment".to_string()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
let range = CachedRange {
@@ -507,15 +491,8 @@ mod tests {
let end1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
let tx1 = Transaction {
transaction_id: Some("tx1".to_string()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2024-01-05".to_string()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: gocardless_client::models::TransactionAmount {
amount: "100.00".to_string(),
currency: "EUR".to_string(),
@@ -523,19 +500,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Creditor".to_string()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Payment".to_string()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
cache.store_transactions(start1, end1, vec![tx1]);
@@ -549,15 +517,8 @@ mod tests {
let end2 = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
let tx2 = Transaction {
transaction_id: Some("tx2".to_string()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2024-01-12".to_string()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: gocardless_client::models::TransactionAmount {
amount: "200.00".to_string(),
currency: "EUR".to_string(),
@@ -565,19 +526,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Creditor2".to_string()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Payment2".to_string()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
cache.store_transactions(start2, end2, vec![tx2]);
@@ -597,16 +549,9 @@ mod tests {
let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
let tx1 = Transaction {
transaction_id: Some("tx1".to_string()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
transaction_id: Some("dup".to_string()),
booking_date: Some("2024-01-05".to_string()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: gocardless_client::models::TransactionAmount {
amount: "100.00".to_string(),
currency: "EUR".to_string(),
@@ -614,19 +559,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Creditor".to_string()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Payment".to_string()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
let tx2 = tx1.clone(); // Duplicate
cache.store_transactions(start, end, vec![tx1, tx2]);
@@ -638,15 +574,8 @@ mod tests {
fn test_get_cached_transactions() {
let tx1 = Transaction {
transaction_id: Some("tx1".to_string()),
entry_reference: None,
end_to_end_id: None,
mandate_id: None,
check_id: None,
creditor_id: None,
booking_date: Some("2024-01-05".to_string()),
value_date: None,
booking_date_time: None,
value_date_time: None,
transaction_amount: gocardless_client::models::TransactionAmount {
amount: "100.00".to_string(),
currency: "EUR".to_string(),
@@ -654,19 +583,10 @@ mod tests {
currency_exchange: None,
creditor_name: Some("Creditor".to_string()),
creditor_account: None,
ultimate_creditor: None,
debtor_name: None,
debtor_account: None,
ultimate_debtor: None,
remittance_information_unstructured: Some("Payment".to_string()),
remittance_information_unstructured_array: None,
remittance_information_structured: None,
remittance_information_structured_array: None,
additional_information: None,
purpose_code: None,
bank_transaction_code: None,
proprietary_bank_transaction_code: None,
internal_transaction_id: None,
};
let range = CachedRange {
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),

View File

@@ -59,24 +59,10 @@ pub struct TransactionBookedPending {
pub struct Transaction {
#[serde(rename = "transactionId")]
pub transaction_id: Option<String>,
#[serde(rename = "entryReference")]
pub entry_reference: Option<String>,
#[serde(rename = "endToEndId")]
pub end_to_end_id: Option<String>,
#[serde(rename = "mandateId")]
pub mandate_id: Option<String>,
#[serde(rename = "checkId")]
pub check_id: Option<String>,
#[serde(rename = "creditorId")]
pub creditor_id: Option<String>,
#[serde(rename = "bookingDate")]
pub booking_date: Option<String>,
#[serde(rename = "valueDate")]
pub value_date: Option<String>,
#[serde(rename = "bookingDateTime")]
pub booking_date_time: Option<String>,
#[serde(rename = "valueDateTime")]
pub value_date_time: Option<String>,
#[serde(rename = "transactionAmount")]
pub transaction_amount: TransactionAmount,
#[serde(rename = "currencyExchange")]
@@ -85,32 +71,14 @@ pub struct Transaction {
pub creditor_name: Option<String>,
#[serde(rename = "creditorAccount")]
pub creditor_account: Option<AccountDetails>,
#[serde(rename = "ultimateCreditor")]
pub ultimate_creditor: Option<String>,
#[serde(rename = "debtorName")]
pub debtor_name: Option<String>,
#[serde(rename = "debtorAccount")]
pub debtor_account: Option<AccountDetails>,
#[serde(rename = "ultimateDebtor")]
pub ultimate_debtor: Option<String>,
#[serde(rename = "remittanceInformationUnstructured")]
pub remittance_information_unstructured: Option<String>,
#[serde(rename = "remittanceInformationUnstructuredArray")]
pub remittance_information_unstructured_array: Option<Vec<String>>,
#[serde(rename = "remittanceInformationStructured")]
pub remittance_information_structured: Option<String>,
#[serde(rename = "remittanceInformationStructuredArray")]
pub remittance_information_structured_array: Option<Vec<String>>,
#[serde(rename = "additionalInformation")]
pub additional_information: Option<String>,
#[serde(rename = "purposeCode")]
pub purpose_code: Option<String>,
#[serde(rename = "bankTransactionCode")]
pub bank_transaction_code: Option<String>,
#[serde(rename = "proprietaryBankTransactionCode")]
pub proprietary_bank_transaction_code: Option<String>,
#[serde(rename = "internalTransactionId")]
pub internal_transaction_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -134,10 +102,4 @@ pub struct CurrencyExchange {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountDetails {
pub iban: Option<String>,
pub bban: Option<String>,
pub pan: Option<String>,
#[serde(rename = "maskedPan")]
pub masked_pan: Option<String>,
pub msisdn: Option<String>,
pub currency: Option<String>,
}