Return data
Return data
The ReturnAggregator rolls a set of assessments up into return-data — net and
tax totals per taxing jurisdiction and currency — ready for filing. The engine
owns the aggregation; submitting to each authority (e.g. via the UK MTD API or an
EU OSS portal) is the host's concern.
A taxing jurisdiction is a country plus, where the tax is sub-federal, its subdivision. So a US set produces a line per state and an EU OSS set produces a line per member state — the granularity a filing actually needs — rather than collapsing everything to the country.
use Cbox\Tax\Contracts\ReturnAggregator;
$return = app(ReturnAggregator::class)->aggregate($assessments);
foreach ($return->lines as $line) {
$line->country->value; // "US"
$line->subdivision?->value; // "US-CA" (null for national lines)
$line->currency; // "USD"
(string) $line->net->getAmount(); // "200.00"
(string) $line->tax->getAmount(); // "14.50"
$line->count; // number of supplies
}
$return->lineFor(new CountryCode('DK'), 'EUR'); // national line
$return->lineFor(new CountryCode('US'), 'USD', new SubdivisionCode('US-CA')); // per-state line
A bare-country lineFor() matches only a national (subdivision-less) line — a US
state line is not returned unless you pass its subdivision.
Money of different currencies is never mixed — each currency is its own line, so a
jurisdiction billed in more than one currency yields more than one line. Summing
uses exact Money::plus, so aggregation introduces no rounding remainder.
Per-authority totals
A jurisdiction total is not what gets remitted in a stacked state. Kansas is paid as a state figure and a city figure, to different authorities, and a line that only said "US-KS: $81.25" left you to rebuild the split by hand from the individual assessments — the one piece of arithmetic on a signed return that should never be done twice.
foreach ($line->authorities ?? [] as $authority) {
$authority->level; // JurisdictionLevel::State | County | City | …
$authority->label(); // the name, else the code, else the level
(string) $authority->tax->getAmount(); // what this authority is owed for the period
}
authorities is null when the split cannot be known, and that is the only safe
reading of it — treat it as missing data, never as "one authority took everything".
It is null when any taxed supply in the period arrived without a rate breakdown, or
carried an authority share with neither a code nor a name: you cannot remit to an
authority you cannot identify.
That is stricter than the equivalent on a single document, deliberately. A document breakdown is there to be read; a return is signed and paid from. A partial roll-up would still add up to a plausible figure, which is exactly what makes the omission invisible.
The jurisdiction-level net, tax and count on the line stay correct either
way — only the split goes unreported.
One line, split by treatment
A period's total for a country is not a filing. ReturnLine::$byTreatment carries
the same line split into what it is made of, so each figure can go to the box that
asks for it:
$line = $return->lineFor(new CountryCode('FR'), 'EUR');
$line->net; // the whole period
$line->forTreatment(TaxTreatment::Standard)?->tax; // what was charged
$line->forTreatment(TaxTreatment::IntraCommunitySupply)?->net; // Art. 138 goods
$line->forTreatment(TaxTreatment::ReverseCharge)?->net; // Art. 196 services
Every treatment but Standard contributes a zero to the line's tax, so a single
total cannot be taken apart again afterwards — which is why the split is built
during aggregation rather than offered as a helper over the result. A treatment the
period never saw is absent, not a zero row: nothing was supplied under it.