Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::charity_gift_aid::gift_aid; ← from charity.gift-aid ^1.0.0 · built alongside by fune
use super::charity_reclaim_batch_data::AGGREGATION; ← this capability’s own data, compiled from data/aggregation.json into the same file by fune build
use super::dates_uk_tax_year::uk_tax_year; ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// The totals of a Gift Aid repayment claim for a period. Every donation is
/// either on the claim or listed as excluded with its reason, lines come out
/// in a fixed order (donor lines by donorId, then aggregated lines by date),
/// and the tax is worked out on each tax year's total.
///
/// # Panics
/// Panics on a malformed date, a period that ends before it starts, a
/// duplicate donation id, a non-GBP donation, or a date with no basic rate.
pub fn gift_aid_claim_batch(
donations: &[GiftAidDonation],
period_start: &str,
period_end: &str,
aggregate_small: bool,
) -> GiftAidClaimBatch {
uk_tax_year(period_start);
uk_tax_year(period_end);
if period_end < period_start {
panic!("periodEnd {} is before periodStart {}", period_end, period_start);
}
let rule = &AGGREGATION[0];
let mut ids: Vec<&str> = Vec::new();
let mut excluded: Vec<ExcludedDonation> = Vec::new();
let mut eligible: Vec<&GiftAidDonation> = Vec::new();
for d in donations {
if ids.contains(&d.id.as_str()) {
panic!("duplicate donation id \"{}\"", d.id);
}
ids.push(&d.id);
if d.amount.currency != "GBP" {
panic!("donation \"{}\" must be in GBP, received {}", d.id, d.amount.currency);
}
uk_tax_year(&d.date);
let reason = if d.date.as_str() < period_start || d.date.as_str() > period_end {
Some("outside-period")
} else if d.amount.minor <= 0 {
Some("not-positive")
} else if !d.has_declaration {
Some("no-declaration")
} else if d.donor_id.is_none() {
Some("no-donor")
} else {
None
};
match reason {
None => eligible.push(d),
Some(r) => excluded.push(ExcludedDonation { id: d.id.clone(), reason: r.to_string() }),
}
}
let (small, named): (Vec<&GiftAidDonation>, Vec<&GiftAidDonation>) = if aggregate_small {
eligible.iter().partition(|d| d.amount.minor <= rule.max_donation)
} else {
(Vec::new(), eligible.clone())
};
let mut donor_ids: Vec<&str> = Vec::new();
for d in &named {
let key = d.donor_id.as_deref().unwrap();
if !donor_ids.contains(&key) {
donor_ids.push(key);
}
}
donor_ids.sort();
let mut lines: Vec<ClaimLine> = Vec::new();
for donor_id in donor_ids {
let group: Vec<&&GiftAidDonation> = named.iter().filter(|d| d.donor_id.as_deref() == Some(donor_id)).collect();
lines.push(ClaimLine {
kind: "donor".to_string(),
donor_id: Some(donor_id.to_string()),
last_date: group.iter().map(|d| d.date.clone()).max().unwrap(),
count: group.len() as i64,
total: money(group.iter().map(|d| d.amount.minor).sum(), "GBP"),
});
}
// Aggregated lines fill in date order (ties by id) up to the line limit.
let mut sorted_small = small.clone();
sorted_small.sort_by(|a, b| a.date.cmp(&b.date).then_with(|| a.id.cmp(&b.id)));
let mut current: Vec<&GiftAidDonation> = Vec::new();
let mut current_total = 0i64;
for d in sorted_small {
if current_total + d.amount.minor > rule.max_line_total && !current.is_empty() {
lines.push(ClaimLine {
kind: "aggregated".to_string(),
donor_id: None,
last_date: current[current.len() - 1].date.clone(),
count: current.len() as i64,
total: money(current_total, "GBP"),
});
current.clear();
current_total = 0;
}
current.push(d);
current_total += d.amount.minor;
}
if !current.is_empty() {
lines.push(ClaimLine {
kind: "aggregated".to_string(),
donor_id: None,
last_date: current[current.len() - 1].date.clone(),
count: current.len() as i64,
total: money(current_total, "GBP"),
});
}
// One rounding per tax year, since each year has its own basic rate.
let mut by_year: Vec<(String, i64)> = Vec::new();
for d in &eligible {
let start = uk_tax_year(&d.date).start;
match by_year.iter_mut().find(|(s, _)| *s == start) {
Some(entry) => entry.1 += d.amount.minor,
None => by_year.push((start, d.amount.minor)),
}
}
by_year.sort();
let claimable: i64 = by_year
.iter()
.map(|(start, total)| gift_aid(&money(*total, "GBP"), start).tax_reclaimable.minor)
.sum();
GiftAidClaimBatch {
lines,
eligible_count: eligible.len() as i64,
eligible_total: money(eligible.iter().map(|d| d.amount.minor).sum(), "GBP"),
claimable: money(claimable, "GBP"),
excluded,
}
}
pub fn gift_aid_donation_from_value(v: &Value) -> GiftAidDonation {
GiftAidDonation {
id: v.get("id").as_str().to_string(),
donor_id: if v.get("donorId").is_null() { None } else { Some(v.get("donorId").as_str().to_string()) },
date: v.get("date").as_str().to_string(),
amount: money_from_value(v.get("amount")),
has_declaration: v.get("hasDeclaration").as_bool(),
}
}
pub fn gift_aid_claim_batch_to_value(b: &GiftAidClaimBatch) -> Value {
Value::obj(vec![
(
"lines",
Value::Arr(
b.lines
.iter()
.map(|l| {
Value::obj(vec![
("kind", Value::str(&l.kind)),
("donorId", match &l.donor_id { Some(id) => Value::str(id), None => Value::Null }),
("lastDate", Value::str(&l.last_date)),
("count", Value::Int(l.count)),
("total", money_to_value(&l.total)),
])
})
.collect(),
),
),
("eligibleCount", Value::Int(b.eligible_count)),
("eligibleTotal", money_to_value(&b.eligible_total)),
("claimable", money_to_value(&b.claimable)),
(
"excluded",
Value::Arr(
b.excluded
.iter()
.map(|e| Value::obj(vec![("id", Value::str(&e.id)), ("reason", Value::str(&e.reason))]))
.collect(),
),
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let donations: Vec<GiftAidDonation> = args[0].as_arr().iter().map(gift_aid_donation_from_value).collect();
gift_aid_claim_batch_to_value(&gift_aid_claim_batch(&donations, args[1].as_str(), args[2].as_str(), args[3].as_bool()))
}