diff --git a/README.md b/README.md index 95a0f92b..91d1d2cc 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,15 @@ Bill and insurance events include `external_ref` where applicable for off-chain Bill and insurance events include `external_ref` where applicable for off-chain linking. +### Reporting + +Provides summarized views of user financial data across all ecosystem contracts. + +**Key Features:** + +- `get_remittance_summary`: Aggregates split, savings, bills, and insurance data into a single summary +- **Graceful Degradation**: Implements a `DataAvailability` indicator (`Complete`, `Partial`, `Missing`) ensuring summary queries return available data without a hard panic if upstream contracts are unresponsive, unconfigured, or fail. + ### Family Wallet Manages family roles, spending controls, multisig approvals, and emergency transfer policies. diff --git a/reporting/src/lib.rs b/reporting/src/lib.rs index 14af5031..18b8321c 100644 --- a/reporting/src/lib.rs +++ b/reporting/src/lib.rs @@ -43,6 +43,18 @@ pub struct TrendData { pub change_percentage: i32, // Can be negative } +/// Indicates the completeness of the data retrieved from external contracts +#[contracttype] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DataAvailability { + /// All external calls succeeded and data is complete + Complete = 0, + /// Some external calls failed or returned partial data + Partial = 1, + /// Critical external calls failed or addresses not configured, data is missing/default + Missing = 2, +} + /// Remittance summary report #[contracttype] #[derive(Clone)] @@ -52,6 +64,7 @@ pub struct RemittanceSummary { pub category_breakdown: Vec, pub period_start: u64, pub period_end: u64, + pub data_availability: DataAvailability, } /// Savings progress report @@ -489,8 +502,20 @@ impl ReportingContract { let addresses: ContractAddresses = env .storage() .instance() - .get(&symbol_short!("ADDRS")) - .unwrap_or_else(|| panic!("Contract addresses not configured")); + .get(&symbol_short!("ADDRS")); + + if addresses.is_none() { + return RemittanceSummary { + total_received: total_amount, + total_allocated: total_amount, + category_breakdown: Vec::new(&env), + period_start, + period_end, + data_availability: DataAvailability::Missing, + }; + } + + let addresses = addresses.unwrap(); let split_client = RemittanceSplitClient::new(env, &addresses.remittance_split); let split_percentages = split_client.get_split(); @@ -518,6 +543,7 @@ impl ReportingContract { category_breakdown: breakdown, period_start, period_end, + data_availability: availability, } } diff --git a/reporting/src/tests.rs b/reporting/src/tests.rs index c474fafe..96d126b2 100644 --- a/reporting/src/tests.rs +++ b/reporting/src/tests.rs @@ -265,7 +265,8 @@ fn test_configure_addresses_succeeds() { #[test] fn test_configure_addresses_unauthorized() { - let env = create_test_env(); + let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register_contract(None, ReportingContract); let client = ReportingContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -332,6 +333,7 @@ fn test_get_remittance_summary() { assert_eq!(summary.category_breakdown.len(), 4); assert_eq!(summary.period_start, period_start); assert_eq!(summary.period_end, period_end); + assert_eq!(summary.data_availability, DataAvailability::Complete); // Check category breakdown let spending = summary.category_breakdown.get(0).unwrap(); @@ -340,6 +342,78 @@ fn test_get_remittance_summary() { assert_eq!(spending.percentage, 50); } +#[test] +fn test_get_remittance_summary_missing_addresses() { + let env = soroban_sdk::Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, ReportingContract); + let client = ReportingContractClient::new(&env, &contract_id); + let user = soroban_sdk::Address::generate(&env); + + // Purposefully DO NOT call client.init() or client.configure_addresses() + + let total_amount = 10000i128; + let period_start = 1704067200u64; + let period_end = 1706745600u64; + + let summary = client.get_remittance_summary(&user, &total_amount, &period_start, &period_end); + + assert_eq!(summary.total_received, 10000); + assert_eq!(summary.category_breakdown.len(), 0); + assert_eq!(summary.data_availability, DataAvailability::Missing); +} + +mod failing_remittance_split { + use soroban_sdk::{contract, contractimpl, Env, Vec}; + #[contract] + pub struct FailingRemittanceSplit; + #[contractimpl] + impl FailingRemittanceSplit { + pub fn get_split(_env: &Env) -> Vec { + panic!("Remote call failing to simulate Partial Data"); + } + pub fn calculate_split(_env: Env, _total_amount: i128) -> Vec { + panic!("Remote call failing to simulate Partial Data"); + } + } +} + +#[test] +fn test_get_remittance_summary_partial_data() { + let env = soroban_sdk::Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, ReportingContract); + let client = ReportingContractClient::new(&env, &contract_id); + let admin = soroban_sdk::Address::generate(&env); + let user = soroban_sdk::Address::generate(&env); + + client.init(&admin); + + // Register FAILING mock contract + let failing_split_id = env.register_contract(None, failing_remittance_split::FailingRemittanceSplit); + let savings_goals_id = env.register_contract(None, savings_goals::SavingsGoalsContract); + let bill_payments_id = env.register_contract(None, bill_payments::BillPayments); + let insurance_id = env.register_contract(None, insurance::Insurance); + let family_wallet = soroban_sdk::Address::generate(&env); + + client.configure_addresses( + &admin, + &failing_split_id, + &savings_goals_id, + &bill_payments_id, + &insurance_id, + &family_wallet, + ); + + let total_amount = 10000i128; + let summary = client.get_remittance_summary(&user, &total_amount, &0, &0); + + assert_eq!(summary.total_received, 10000); + assert_eq!(summary.category_breakdown.len(), 4); // Created empty via fallback + assert_eq!(summary.category_breakdown.get(0).unwrap().amount, 0); + assert_eq!(summary.data_availability, DataAvailability::Partial); +} + #[test] fn test_get_savings_report() { let env = Env::default(); @@ -954,7 +1028,8 @@ fn test_storage_stats_regression_across_archive_and_cleanup_cycles() { #[test] fn test_archive_unauthorized() { - let env = create_test_env(); + let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register_contract(None, ReportingContract); let client = ReportingContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -969,7 +1044,8 @@ fn test_archive_unauthorized() { #[test] fn test_cleanup_unauthorized() { - let env = create_test_env(); + let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register_contract(None, ReportingContract); let client = ReportingContractClient::new(&env, &contract_id); let admin = Address::generate(&env); diff --git a/reporting/test_snapshots/tests/test_archive_empty_when_no_old_reports.1.json b/reporting/test_snapshots/tests/test_archive_empty_when_no_old_reports.1.json index ee7322d0..bad5a487 100644 --- a/reporting/test_snapshots/tests/test_archive_empty_when_no_old_reports.1.json +++ b/reporting/test_snapshots/tests/test_archive_empty_when_no_old_reports.1.json @@ -143,7 +143,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -230,7 +230,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_archive_old_reports.1.json b/reporting/test_snapshots/tests/test_archive_old_reports.1.json index a3a0c3f0..0f8e7237 100644 --- a/reporting/test_snapshots/tests/test_archive_old_reports.1.json +++ b/reporting/test_snapshots/tests/test_archive_old_reports.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -875,7 +883,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -1288,7 +1296,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -3256,6 +3264,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3760,6 +3776,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -4351,6 +4375,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json b/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json index 364cd25d..61cc9f2b 100644 --- a/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json +++ b/reporting/test_snapshots/tests/test_archive_ttl_extended_on_archive_reports.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3095,6 +3103,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3599,6 +3615,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_calculate_health_score.1.json b/reporting/test_snapshots/tests/test_calculate_health_score.1.json index 0f26ab1b..b33a9203 100644 --- a/reporting/test_snapshots/tests/test_calculate_health_score.1.json +++ b/reporting/test_snapshots/tests/test_calculate_health_score.1.json @@ -180,7 +180,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -428,7 +428,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json b/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json index 518b59b2..3e664244 100644 --- a/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json +++ b/reporting/test_snapshots/tests/test_cleanup_old_reports.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -815,7 +823,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -1228,7 +1236,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -3196,6 +3204,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3700,6 +3716,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_get_bill_compliance_report.1.json b/reporting/test_snapshots/tests/test_get_bill_compliance_report.1.json index aa6c905b..4fbb79fd 100644 --- a/reporting/test_snapshots/tests/test_get_bill_compliance_report.1.json +++ b/reporting/test_snapshots/tests/test_get_bill_compliance_report.1.json @@ -180,7 +180,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -428,7 +428,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_get_financial_health_report.1.json b/reporting/test_snapshots/tests/test_get_financial_health_report.1.json index fc2153aa..a035f7e6 100644 --- a/reporting/test_snapshots/tests/test_get_financial_health_report.1.json +++ b/reporting/test_snapshots/tests/test_get_financial_health_report.1.json @@ -186,7 +186,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -434,7 +434,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -2402,6 +2402,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_get_insurance_report.1.json b/reporting/test_snapshots/tests/test_get_insurance_report.1.json index 7a5b7fc4..79384e86 100644 --- a/reporting/test_snapshots/tests/test_get_insurance_report.1.json +++ b/reporting/test_snapshots/tests/test_get_insurance_report.1.json @@ -180,7 +180,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -428,7 +428,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_get_remittance_summary.1.json b/reporting/test_snapshots/tests/test_get_remittance_summary.1.json index 38399985..a3b679af 100644 --- a/reporting/test_snapshots/tests/test_get_remittance_summary.1.json +++ b/reporting/test_snapshots/tests/test_get_remittance_summary.1.json @@ -186,7 +186,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -434,7 +434,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -909,6 +909,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_get_savings_report.1.json b/reporting/test_snapshots/tests/test_get_savings_report.1.json index dfdc5380..4f8a150c 100644 --- a/reporting/test_snapshots/tests/test_get_savings_report.1.json +++ b/reporting/test_snapshots/tests/test_get_savings_report.1.json @@ -180,7 +180,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -428,7 +428,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_health_score_no_goals.1.json b/reporting/test_snapshots/tests/test_health_score_no_goals.1.json index f6045507..971dcecf 100644 --- a/reporting/test_snapshots/tests/test_health_score_no_goals.1.json +++ b/reporting/test_snapshots/tests/test_health_score_no_goals.1.json @@ -180,7 +180,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -428,7 +428,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] diff --git a/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json b/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json index 803cadf1..bf45df97 100644 --- a/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json +++ b/reporting/test_snapshots/tests/test_instance_ttl_refreshed_on_store_report.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -1057,6 +1065,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3464,6 +3480,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3968,6 +3992,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json b/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json index 7c6372a4..6f8d52ae 100644 --- a/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json +++ b/reporting/test_snapshots/tests/test_report_data_persists_across_ledger_advancements.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -991,6 +999,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -1631,6 +1647,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -2119,6 +2143,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -4658,6 +4690,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -5162,6 +5202,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -7165,6 +7213,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -7669,6 +7725,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -8260,6 +8324,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -8789,6 +8861,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_storage_stats.1.json b/reporting/test_snapshots/tests/test_storage_stats.1.json index 76d04d38..f4c06df0 100644 --- a/reporting/test_snapshots/tests/test_storage_stats.1.json +++ b/reporting/test_snapshots/tests/test_storage_stats.1.json @@ -463,6 +463,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -815,7 +823,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -1129,7 +1137,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -3169,6 +3177,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -3673,6 +3689,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" diff --git a/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json b/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json index e5413422..ac4cb5e8 100644 --- a/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json +++ b/reporting/test_snapshots/tests/test_store_and_retrieve_report.1.json @@ -462,6 +462,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -1078,6 +1086,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -1236,7 +1252,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ], [ @@ -1550,7 +1566,7 @@ }, "ext": "v0" }, - 518401 + 100000 ] ] ] @@ -3518,6 +3534,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -4022,6 +4046,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end" @@ -4613,6 +4645,14 @@ ] } }, + { + "key": { + "symbol": "data_availability" + }, + "val": { + "u32": 0 + } + }, { "key": { "symbol": "period_end"