crates/monetize-server/src/services/tax.rs
//! **`Tax.Periods` / `Tax.Report`** — the ledger as the jurisdiction's boxes.
//!
//! Both calls fold the WHOLE ledger (`monetize::Books::view`), which is a full
//! scan and is meant to be: a return covers a period that has closed, and the
//! bounded recent-sales walk every other read uses would silently drop the
//! oldest sales in it. A return is asked for a handful of times a year, by an
//! operator, on a blocking thread like every other ledger read here.
//!
//! The jurisdiction is whatever `Linked::tax` put in the state. With none
//! linked and none declared that is `monetize::PerCountry`: each buyer
//! country's totals, from the country, rate and tax every payment row carries.
use std::sync::Arc;
use monetize::tax::{Amount, Period};
use monetize_proto::tax_server::Tax;
use monetize_proto::{self as pb};
use tonic::{Request, Response, Status};
use super::{blocking, ledger_err};
use crate::convert::money_to_pb;
use crate::state::AppState;
pub struct TaxService(pub Arc<AppState>);
/// **Which year's turnover chooses the period.** The year BEFORE the one being
/// filed: a company's reporting frequency for 2026 follows what it turned over
/// in 2025, and a rule applied to the year in progress would change its own
/// answer every time a sale landed.
fn turnover_year(year: u16) -> u16 {
year.saturating_sub(1)
}
/// A year a caller may ask about. Anything outside it is a typo or a probe, and
/// the period arithmetic below would happily produce twelve empty months for
/// year 0.
fn year_or_invalid(year: u32) -> Result<u16, Status> {
match year {
1970..=9999 => Ok(year as u16),
_ => Err(Status::invalid_argument(format!(
"{year} is not a year monetize can file for; 1970..=9999"
))),
}
}
fn period_to_pb(p: &Period) -> pb::TaxPeriod {
pb::TaxPeriod {
label: p.label.clone(),
kind: p.kind.name().into(),
from_unix_ms: p.from_unix_ms,
to_unix_ms: p.to_unix_ms,
}
}
fn line_to_pb(l: &monetize::tax::Line) -> pb::TaxLine {
pb::TaxLine {
box_id: l.box_id.clone(),
label: l.label.clone(),
// Absent stays absent all the way to the browser: `—` is not 0.
amount_minor: l.amount.map(|a: Amount| a.minor),
currency: l
.amount
.map(|a| a.currency.code().to_owned())
.unwrap_or_default(),
}
}
/// **The ledger AND the paid signups**, as one view. A signup writes no ledger
/// row, and a return folded from the ledger alone declared none of them
/// (2026-09-15) — see [`crate::signup::tax_sales`].
fn tax_view(s: &AppState) -> Result<monetize::tax::LedgerView, Status> {
let books = s.books.read().unwrap_or_else(|e| e.into_inner()).clone();
let mut view = books.view(&s.ledger).map_err(ledger_err)?;
view.sales.extend(
crate::signup::tax_sales(&s.signups, &books)
.map_err(|e| Status::internal(format!("signup store: {e}")))?,
);
Ok(view)
}
#[tonic::async_trait]
impl Tax for TaxService {
async fn periods(
&self,
req: Request<pb::TaxPeriodsRequest>,
) -> Result<Response<pb::TaxPeriodsResponse>, Status> {
let year = year_or_invalid(req.into_inner().year)?;
blocking(&self.0, move |s| {
let view = tax_view(s)?;
// The turnover the rule is applied to, in the currency the
// deployment prices in. A jurisdiction whose thresholds are in
// another currency refuses inside `periods`, by name.
let currency = s
.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market
.currency();
let turnover = view.turnover_of_year(currency, turnover_year(year));
let periods = s.tax.periods(turnover, year);
Ok(Response::new(pb::TaxPeriodsResponse {
jurisdiction: s.tax.jurisdiction().to_owned(),
periods: periods.iter().map(period_to_pb).collect(),
turnover: Some(money_to_pb(turnover)),
turnover_year: turnover_year(year) as u32,
}))
})
.await
}
async fn report(
&self,
req: Request<pb::TaxReportRequest>,
) -> Result<Response<pb::TaxReportResponse>, Status> {
let r = req.into_inner();
let year = year_or_invalid(r.year)?;
if r.period.is_empty() {
return Err(Status::invalid_argument("report needs a period label from Tax.Periods; a return for the wrong period is worse than none"));
}
blocking(&self.0, move |s| {
let view = tax_view(s)?;
let currency = s
.terms
.read()
.unwrap_or_else(|e| e.into_inner())
.default_market
.currency();
let turnover = view.turnover_of_year(currency, turnover_year(year));
// THE PERIOD IS RESOLVED FROM THE JURISDICTION'S OWN LIST, never
// parsed out of the label. A label the deployment does not file is
// refused by name — `2026-Q3` on a monthly filer is not a period
// that exists, and reporting it as if it did would produce a figure
// for a form nobody sends.
let offered = s.tax.periods(turnover, year);
let period = offered
.iter()
.find(|p| p.label == r.period)
.ok_or_else(|| {
Status::not_found(format!(
"{:?} is not a period {} files for {year}; it files {:?}",
r.period,
s.tax.jurisdiction(),
offered.iter().map(|p| p.label.as_str()).collect::<Vec<_>>()
))
})?;
let report = s
.tax
.report(&view, period)
.map_err(|e| Status::failed_precondition(e.to_string()))?;
Ok(Response::new(pb::TaxReportResponse {
jurisdiction: report.jurisdiction.clone(),
period: Some(period_to_pb(&report.period)),
lines: report.lines.iter().map(line_to_pb).collect(),
notes: report.notes.clone(),
// One writer for the table and the file: the page shows
// `lines`, the accountant gets this, and they are the same fold.
csv: report.to_csv(),
}))
})
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_period_rule_looks_at_the_year_before_the_one_being_filed() {
assert_eq!(turnover_year(2026), 2025);
assert_eq!(
turnover_year(0),
0,
"saturating, so no year underflows into 65535"
);
}
#[test]
fn a_year_outside_the_window_is_refused_rather_than_filed_for() {
assert_eq!(year_or_invalid(2026).unwrap(), 2026);
assert!(year_or_invalid(0).is_err());
assert!(year_or_invalid(10_000).is_err());
}
/// The wire keeps `—` as absent. A line that arrived as 0 would read as a
/// figure somebody computed.
#[test]
fn an_uncomputed_box_travels_as_absent_and_not_as_zero() {
let absent = line_to_pb(&monetize::tax::Line::absent("49", "to pay or get back"));
assert_eq!(absent.amount_minor, None);
assert_eq!(absent.currency, "");
let zero = line_to_pb(&monetize::tax::Line::new(
"49",
"to pay or get back",
Amount::zero(monetize_pay::Currency::Sek),
));
assert_eq!(zero.amount_minor, Some(0));
assert_eq!(zero.currency, "SEK");
}
#[test]
fn a_period_travels_with_its_kind_as_a_word() {
let p = monetize::tax::PeriodKind::Quarterly.periods(2026).remove(2);
let wire = period_to_pb(&p);
assert_eq!(
(wire.label.as_str(), wire.kind.as_str()),
("2026-Q3", "quarterly")
);
assert_eq!(
wire.to_unix_ms,
monetize_pay::time::ymd_to_unix_ms(2026, 10, 1).unwrap()
);
}
}