Tax & VIES
Per-(category × market) tax rates expressed in basis points, plus VIES validation for B2B EU customers with reverse-charge handling.
Tax categories
A category classifies a product for tax purposes. Common codes (account-defined):
| Code | Czech name | Typical rate |
|---|---|---|
standard | DPH 21% | 21% |
reduced | DPH 12% | 12% |
super_reduced | DPH 0% | 0% |
zero | Bez DPH | 0% |
exempt | Osvobozeno | — |
curl -X POST "$API/tax/categories.json" \
-H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
-d '{"code":"standard","name":"DPH 21 %","is_default":true}'
Each variant is implicitly mapped via its tax_rate_bps. (Explicit
mapping table coming in a future iteration.)
Tax rates
Rates are basis points: 2100 = 21.00%. Per (category × market).
curl -X POST "$API/tax/rates.json" \
-H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
-d '{
"tax_category_id": 308...,
"market_id": 308...,
"rate_bps": 2100,
"display_name": "DPH 21 %"
}'
When you set a new rate, the previous active one for that (category,
market) is auto-closed (effective_to = now()) — no overlap. The
historical row stays for reporting.
find_active_rate(category, market, when) walks the timeline so a
3-year-old order can still report its original tax rate even if the
current rate is different.
VIES (B2B reverse charge)
The vat_validations table is an audit log of every VIES check.
Sources: vies (live), manual (staff override), cached (we
trusted a recent cached result).
curl -X POST "$API/tax/vat/validate.json" \
-H "Authorization: Bearer $NEVIOS_KEY" -H "Content-Type: application/json" \
-d '{
"vat_country_code": "CZ",
"vat_number": "12345678",
"customer_id": 308...,
"use_cache": true
}'
{
"vat_validation": {
"id": "308...",
"vat_country_code": "CZ",
"vat_number": "12345678",
"valid": true,
"registered_name": null,
"registered_address": null,
"consultation_number": null,
"source": "vies",
"validated_at": "2026-05-01T10:30:00Z"
}
}
Phase 1 stub: the live VIES SOAP call is currently mocked — it
returns valid: true for syntactically-valid EU country VAT numbers
and false otherwise. A real SOAP client lands in the next iteration.
The cache window is configurable per account
(commerce.tax.vies_cache_days setting, default 7 days). Cache hits
return the previous validation row instead of re-checking.
Reverse charge at checkout
When the customer enters a VAT number at checkout
(PUT /storefront/2026-01/checkouts/{token}/vat.json), the checkout
service:
- Calls
validate_vat_number(cache → live) - Sets
vat_validation_id - Computes
reverse_charge = (valid AND non-domestic AND commerce.tax.b2b_reverse_charge=true) - Sets
tax_exempt = reverse_charge - Recomputes totals — VAT zeroed, prices presented net
Czech B2B sale within CZ → reverse charge off (domestic). Czech B2B sale to a German VAT-registered customer → reverse charge on (EU non-domestic + valid VAT).
Fail-open with cache
If VIES SOAP is down (real outages happen) and there's no cache hit,
we honor the storefront's use_cache: true setting and fail open
— the validation record gets source='cached' flagged so accountants
can re-verify manually later. This protects revenue during VIES
outages without compromising audit.
Endpoints
POST /admin/2026-01/{handle}/tax/categories.json
POST /admin/2026-01/{handle}/tax/rates.json
GET /admin/2026-01/{handle}/tax/rates.json?active_only=true
POST /admin/2026-01/{handle}/tax/vat/validate.json
Next: Carts API.