OpenPay (3 crates, ~13k LOC, 175 tests) op-connect sub-merchant onboarding + KYB + split-payments + payouts + 1099-K op-3ds2 3DS2.3 SCA processor (AReq/ARes/CReq/CRes/RReq/RRes + exemptions) op-tax US/EU/UK/AU/CA/IN tax with Avalara/Stripe/CCH/Vertex/TaxJar adapters Smart Byte (3 crates, ~7k LOC, 57 tests) smart-byte-libp2p libp2p transport (kad/gossipsub/mdns/autonat/relay/dcutr/identify/ping/req_resp) smart-byte-crdt LWW/GCounter/PnCounter/OrSet/LwwMap/RgaList/2P-Set + HLC + vector clock + Automerge/Yjs interop smart-byte-keri-witness KERI key-rotation cycle + witness layer + watchers + recovery + delegation EOC (3 crates, ~8k LOC, 108 tests) eoc-tools function-calling across Anthropic/OpenAI/Google/Mistral/Cohere + parallel exec + sandbox eoc-multimodal vision (CLIP/SigLIP/ColPali) + audio (Whisper/MMS) + video sampling + modality router eoc-spec-decode vanilla + SpS-with-temp + lookahead speculative decoding + EAGLE/Medusa stubs All three workspaces compile cleanly. Forbidden-unsafe, no unwrap outside tests. Co-Authored-By: Claude Opus 4.7 <[email protected]>
140 lines
4.6 KiB
Rust
140 lines
4.6 KiB
Rust
//! EU B2B reverse-charge predicate.
|
|
//!
|
|
//! Under EU Council Directive 2006/112/EC (Articles 44 & 196), a B2B
|
|
//! cross-border supply of services or goods between two EU member
|
|
//! states shifts the VAT-collection obligation from the seller to
|
|
//! the buyer. The seller invoices at zero rate and the buyer self-
|
|
//! accounts on its own VAT return.
|
|
//!
|
|
//! Conditions (all must hold):
|
|
//! 1. Seller and buyer are in different EU member states.
|
|
//! 2. The buyer is a business with a valid VAT identification number
|
|
//! (we treat "non-empty `tax_id`" as the seller's evidence of #2 —
|
|
//! operators that require live VIES validation should wrap this).
|
|
//! 3. The supply is one of the categories the directive covers
|
|
//! (services, most cross-border B2B goods). We default to "yes"
|
|
//! for every category except `MotorFuel` and other excise items,
|
|
//! where domestic excise still applies on cross-border movement.
|
|
//!
|
|
//! ## What this module does not do
|
|
//!
|
|
//! - **VIES VAT-number validation.** Operators wire that themselves
|
|
//! against the European Commission's VIES SOAP service before
|
|
//! accepting a buyer's tax-ID for reverse-charge purposes.
|
|
//! - **Domestic reverse-charge regimes** (UK construction, certain
|
|
//! gold supplies). Out of scope for v1; add per-jurisdiction when
|
|
//! needed.
|
|
|
|
use crate::calculator::{CustomerType, TaxContext, TaxableLine};
|
|
use crate::category::ProductTaxCategory;
|
|
|
|
/// Returns true if this line qualifies for EU B2B reverse charge —
|
|
/// the seller zero-rates and the buyer self-accounts.
|
|
#[must_use]
|
|
pub fn applies(line: &TaxableLine, ctx: &TaxContext) -> bool {
|
|
let CustomerType::Business { tax_id } = &ctx.customer_type else {
|
|
return false;
|
|
};
|
|
if tax_id.trim().is_empty() {
|
|
return false;
|
|
}
|
|
let to_eu = line.ship_to.is_eu_member();
|
|
let Some(from) = &line.ship_from else {
|
|
return false;
|
|
};
|
|
let from_eu = from.is_eu_member();
|
|
if !(to_eu && from_eu) {
|
|
return false;
|
|
}
|
|
if from.country == line.ship_to.country {
|
|
return false;
|
|
}
|
|
// Excise-bearing categories are NOT reverse-charge eligible —
|
|
// domestic excise duty still applies at the destination.
|
|
!matches!(
|
|
line.category,
|
|
ProductTaxCategory::MotorFuel | ProductTaxCategory::Alcohol | ProductTaxCategory::Tobacco
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::jurisdiction::Jurisdiction;
|
|
use chrono::NaiveDate;
|
|
use op_core::{Currency, Money};
|
|
use std::collections::HashSet;
|
|
|
|
fn line(from: Option<Jurisdiction>, to: Jurisdiction, cat: ProductTaxCategory) -> TaxableLine {
|
|
TaxableLine {
|
|
line_id: "L1".into(),
|
|
amount: Money::from_minor(10_000, Currency::EUR),
|
|
category: cat,
|
|
ship_from: from,
|
|
ship_to: to,
|
|
}
|
|
}
|
|
|
|
fn business_ctx() -> TaxContext {
|
|
TaxContext {
|
|
transaction_date: NaiveDate::parse_from_str("2026-06-15", "%Y-%m-%d").unwrap(),
|
|
customer_type: CustomerType::Business {
|
|
tax_id: "FR12345678901".into(),
|
|
},
|
|
exemption_certs: vec![],
|
|
nexus_jurisdictions: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cross_border_eu_b2b_triggers_reverse_charge() {
|
|
let l = line(
|
|
Some(Jurisdiction::country("DE")),
|
|
Jurisdiction::country("FR"),
|
|
ProductTaxCategory::Saas,
|
|
);
|
|
assert!(applies(&l, &business_ctx()));
|
|
}
|
|
|
|
#[test]
|
|
fn domestic_eu_b2b_does_not_trigger() {
|
|
let l = line(
|
|
Some(Jurisdiction::country("DE")),
|
|
Jurisdiction::country("DE"),
|
|
ProductTaxCategory::Saas,
|
|
);
|
|
assert!(!applies(&l, &business_ctx()));
|
|
}
|
|
|
|
#[test]
|
|
fn cross_border_consumer_does_not_trigger() {
|
|
let l = line(
|
|
Some(Jurisdiction::country("DE")),
|
|
Jurisdiction::country("FR"),
|
|
ProductTaxCategory::Saas,
|
|
);
|
|
let mut ctx = business_ctx();
|
|
ctx.customer_type = CustomerType::Consumer;
|
|
assert!(!applies(&l, &ctx));
|
|
}
|
|
|
|
#[test]
|
|
fn cross_border_to_non_eu_does_not_trigger() {
|
|
let l = line(
|
|
Some(Jurisdiction::country("DE")),
|
|
Jurisdiction::country("CH"),
|
|
ProductTaxCategory::Saas,
|
|
);
|
|
assert!(!applies(&l, &business_ctx()));
|
|
}
|
|
|
|
#[test]
|
|
fn excise_category_does_not_trigger_reverse_charge() {
|
|
let l = line(
|
|
Some(Jurisdiction::country("DE")),
|
|
Jurisdiction::country("FR"),
|
|
ProductTaxCategory::Alcohol,
|
|
);
|
|
assert!(!applies(&l, &business_ctx()));
|
|
}
|
|
}
|