feat(vat): EU place-of-supply VAT, per-payment snapshot, retire vm_payment#161
Merged
Conversation
v0l
force-pushed
the
feat/vat-place-of-supply
branch
4 times, most recently
from
July 15, 2026 14:30
b2e2ab3 to
6284fb4
Compare
v0l
added a commit
to LNVPS/web
that referenced
this pull request
Jul 15, 2026
Add VAT section covering reverse charge for EU business customers, IP-based location determination, and non-EU out-of-scope treatment, aligning with LNVPS/api#161.
v0l
force-pushed
the
feat/vat-place-of-supply
branch
from
July 15, 2026 14:51
6284fb4 to
ca4304e
Compare
… vm_payment Implements EU VAT and records the tax basis on each payment. The seller country is taken from the company's own VAT number (tax_id, i.e. its VIES registration) when set, otherwise from the company country_code. EU VAT applies only when that country is in the EU VAT area; a non-EU seller (e.g. a US company) charges no tax here (other systems such as US sales tax are not handled). - Rate selection (determine_tax): customer with a VAT number -> same country as seller = seller rate, other EU country = 0%, non-EU = 0%; otherwise customer country (self-declared, else IP-derived) -> EU country rate, non-EU = 0%; no customer country -> seller-country fallback rate. - Location evidence: ClientIp extractor (X-Forwarded-For/X-Real-IP) + local MaxMind CountryResolver, captured on account updates and both VM-order endpoints, stored on the user separately from the self-declared country. - Rates: a single shared VatClient (Arc-backed in-memory cache) is created once per process, refreshed at startup and daily, and injected into SubscriptionHandler so every PricingEngine shares the same cache. The static tax_rate config is removed. Rates are 0 until the first refresh. (VIES number validation and refund math use a standalone client, no cache needed.) - Per-payment record: each subscription_payment stores a per-line tax_breakdown (JSON), a uniform summary (tax_rate/tax_country_code/tax_treatment, NULL when lines differ) and the customer country signals (tax_evidence). Surfaced in admin time-series reports. - Invoices show the applied rate; EU-specific reverse-charge (Article 196) and out-of-scope notes render only for EU sellers. - Retire the defunct vm_payment table: drop it, remove models, queries and DTO conversions. ApiVmPayment response shape and the vm_payment RBAC resource are unchanged. Demo data writes subscription_payment. - Remove the completed VM->subscription startup backfill and its migration-only DB helpers (VmForMigration etc.). - README notes the EU-only scope, the seller-from-VAT-number rule, and that this is not tax advice. Behaviour change: non-EU customers/sellers are 0% (the placeholder US 1% map entry is gone).
v0l
force-pushed
the
feat/vat-place-of-supply
branch
from
July 15, 2026 15:17
ca4304e to
434050d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements EU VAT correctly for an IE/OSS-registered company and rolls in the retirement of the defunct
vm_paymenttable.Previously the tax charged was a flat lookup in a 2-entry config map (
IE 23%, placeholderUS 1%) keyed on the user's self-declared country — everything else was 0%, with no location evidence and no reverse-charge handling.What changed
Place-of-supply determination (
PricingEngine::determine_tax)Location evidence (#1) —
ClientIpextractor (X-Forwarded-For/X-Real-IP) + local MaxMindCountryResolver, captured on account updates and both VM-order endpoints, stored separately from the self-declared country.Live rate table (#3) —
EuVatClient→ cloneableVatClientwith anArcrate cache, refreshed at startup + daily. Statictax_rateconfig removed. VAT is 0% until the first successful refresh.Per-payment snapshot (#4) — each
subscription_paymentfreezes a per-line-itemtax_breakdown(JSON, authoritative), a uniform summary (tax_rate/tax_country_code/tax_treatment, NULL when a payment mixes rates/treatments), and customertax_evidence. The per-line design future-proofs payments whose lines resolve to different sellers. Surfaced in admin time-series reports.Invoices (#5) — show the applied VAT rate line and print reverse-charge (Article 196) and out-of-scope legal notes.
vm_paymentretirement — the table is dropped; the backfill payment phase, models, queries and DTO conversions are removed (theApiVmPaymentresponse shape andvm_paymentRBAC resource are unchanged). Demo data now writessubscription_payment.Behaviour change
Non-EU customers are now correctly out of scope (0%) (the placeholder
US 1%map entry is gone).Migrations (irreversible)
20260716000000_user_geo_location— geo columns onusers20260716120000_subscription_payment_vat_snapshot— VAT snapshot columns20260716130000_drop_vm_payment— DROP TABLE vm_payment (assumes prod is already backfilled intosubscription_payment)Tests
400 unit/integration tests pass (incl. an end-to-end renewal asserting the snapshot is persisted), clippy + fmt clean.
lnvps_e2erequires a live stack and is not run here.Get the final policy — especially the "undetermined default" rule and the two-evidence requirement — signed off by an Irish VAT/OSS accountant before relying on it.