Revert "docs: add VAT implementation design"
This reverts commit 4e9439770f.
This commit is contained in:
@@ -1,156 +0,0 @@
|
|||||||
# VAT Implementation Design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Add EU VAT handling to the Pulse checkout and subscription flow. Prices are shown and charged excl. VAT; VAT is calculated at checkout based on customer country and VAT ID, and added to the Mollie charge.
|
|
||||||
|
|
||||||
## VAT Rules
|
|
||||||
|
|
||||||
| Customer | Rate | Reason |
|
|
||||||
|----------|------|--------|
|
|
||||||
| Country = BE | 21% | Belgian VAT |
|
|
||||||
| EU country + valid VIES VAT ID | 0% | Reverse charge (intra-community) |
|
|
||||||
| EU country + no/invalid VAT ID | 21% | Belgian VAT (no OSS registration) |
|
|
||||||
| Non-EU country | 0% | Outside EU scope |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Backend-only VAT calculation. Frontend calls a new endpoint when country/VAT ID changes and displays the breakdown. Backend validates VIES, calculates totals, and charges the VAT-inclusive amount through Mollie.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section 1: New Endpoint — `POST /api/billing/calculate-vat`
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"plan_id": "solo",
|
|
||||||
"interval": "month",
|
|
||||||
"limit": 10000,
|
|
||||||
"country": "BE",
|
|
||||||
"vat_id": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"base_amount": "7.00",
|
|
||||||
"vat_rate": 21,
|
|
||||||
"vat_amount": "1.47",
|
|
||||||
"total_amount": "8.47",
|
|
||||||
"vat_exempt": false,
|
|
||||||
"vat_reason": ""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
VAT-exempt example (valid EU VAT ID):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"base_amount": "7.00",
|
|
||||||
"vat_rate": 0,
|
|
||||||
"vat_amount": "0.00",
|
|
||||||
"total_amount": "7.00",
|
|
||||||
"vat_exempt": true,
|
|
||||||
"vat_reason": "Reverse charge (intra-community)"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section 2: New Backend Module — `internal/billing/vat.go`
|
|
||||||
|
|
||||||
**Functions:**
|
|
||||||
- `CalculateVAT(planID, interval string, limit int64, country, vatID string) (*VATResult, error)` — main entry point
|
|
||||||
- `validateVIES(countryCode, vatNumber string) (bool, error)` — calls EU VIES REST API (`https://ec.europa.eu/taxation_customs/vies/rest-api/ms/{country}/vat/{number}`)
|
|
||||||
- `isEUCountry(country string) bool` — static list of 27 EU member state codes
|
|
||||||
- In-memory VIES cache: 24h TTL per `countryCode+vatNumber` key
|
|
||||||
|
|
||||||
**`EmbeddedCheckoutHandler` changes:**
|
|
||||||
- Call `CalculateVAT` instead of `GetSubscriptionAmount` for the subscription amount
|
|
||||||
- Store VAT breakdown in payment metadata: `vat_rate`, `vat_amount`, `base_amount`, `total_amount`
|
|
||||||
|
|
||||||
**`handleFirstPaymentCompleted` changes:**
|
|
||||||
- Read `total_amount` from metadata for the recurring subscription charge (not `amount`)
|
|
||||||
|
|
||||||
**`ChangePlanHandler` changes:**
|
|
||||||
- Call `CalculateVAT` using stored `billing_country` + `vat_id` from `organization_billing`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section 3: Frontend Changes
|
|
||||||
|
|
||||||
**State lifting in `CheckoutContent` (`app/checkout/page.tsx`):**
|
|
||||||
- `country` and `vatId` state lifted up from `PaymentForm` to `CheckoutContent`
|
|
||||||
- Passed down to both `PlanSummary` (display) and `PaymentForm` (submission)
|
|
||||||
|
|
||||||
**`PlanSummary.tsx`:**
|
|
||||||
- Accepts `country`, `vatId`, `onCountryChange`, `onVatIdChange` props
|
|
||||||
- Country selector and VAT ID input moved here from `PaymentForm`
|
|
||||||
- Calls `POST /api/billing/calculate-vat` on country/VAT ID change (debounced 400ms)
|
|
||||||
- Shows VAT breakdown when country is selected:
|
|
||||||
```
|
|
||||||
€7.00 /mo excl. VAT
|
|
||||||
VAT 21% €1.47
|
|
||||||
──────────────────────
|
|
||||||
Total €8.47 /mo
|
|
||||||
```
|
|
||||||
- Shows "Reverse charge" note when VAT-exempt
|
|
||||||
- Loading spinner while VIES validates
|
|
||||||
|
|
||||||
**`PaymentForm.tsx`:**
|
|
||||||
- Country + VAT ID inputs removed
|
|
||||||
- Receives `totalAmount`, `country`, `vatId` as props
|
|
||||||
- Submit button shows: "Start free trial · €8.47/mo"
|
|
||||||
- Passes `country` + `vat_id` to `createEmbeddedCheckout` as before
|
|
||||||
|
|
||||||
**`PricingSection.tsx`:**
|
|
||||||
- Add small "excl. VAT" label under each plan price
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User selects country / enters VAT ID
|
|
||||||
→ PlanSummary debounce 400ms
|
|
||||||
→ POST /api/billing/calculate-vat
|
|
||||||
→ CalculateVAT()
|
|
||||||
→ validateVIES() if VAT ID provided (cached 24h)
|
|
||||||
→ return VATResult
|
|
||||||
→ Display breakdown in PlanSummary
|
|
||||||
→ Pass totalAmount to PaymentForm submit button
|
|
||||||
|
|
||||||
User clicks "Start free trial"
|
|
||||||
→ POST /api/billing/checkout-embedded (country + vat_id + card_token)
|
|
||||||
→ CalculateVAT() (re-validated server-side)
|
|
||||||
→ CreateFirstPaymentWithToken("0.00") — trial mandate, no charge
|
|
||||||
→ handleFirstPaymentCompleted()
|
|
||||||
→ CreateSubscription(totalAmount) — VAT-inclusive recurring amount
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
- VIES API down → treat as invalid VAT ID (charge 21%) — log warning
|
|
||||||
- VIES validation failed → show "VAT ID could not be verified, 21% VAT will apply"
|
|
||||||
- Country not selected → no VAT breakdown shown, checkout blocked
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
| Action | File |
|
|
||||||
|--------|------|
|
|
||||||
| New | `pulse-backend/internal/billing/vat.go` |
|
|
||||||
| New | `POST /api/billing/calculate-vat` in `billing_handlers.go` |
|
|
||||||
| Modified | `EmbeddedCheckoutHandler` in `billing_handlers.go` |
|
|
||||||
| Modified | `handleFirstPaymentCompleted` in `billing_handlers.go` |
|
|
||||||
| Modified | `ChangePlanHandler` in `billing_handlers.go` |
|
|
||||||
| Modified | `pulse-frontend/app/checkout/page.tsx` |
|
|
||||||
| Modified | `pulse-frontend/components/checkout/PlanSummary.tsx` |
|
|
||||||
| Modified | `pulse-frontend/components/checkout/PaymentForm.tsx` |
|
|
||||||
| Modified | `pulse-frontend/components/PricingSection.tsx` |
|
|
||||||
Reference in New Issue
Block a user