Skip to content
SaaS4Builders
Billing

Tax Configuration

Stripe Tax integration in SaaS4Builders: automatic tax calculation, VAT validation, reverse charge detection, and the TaxProvider abstraction.

SaaS4Builders integrates with Stripe Tax for automatic tax calculation. Stripe Tax is a tax calculation service — it determines which taxes apply and how much to charge. It is not a Merchant of Record. Your SaaS application remains the seller of record, which means you are responsible for invoice generation, tax reporting, and legal compliance.


Responsibility Split

Understanding what Stripe Tax handles versus what your application must handle is critical for compliance:

What Stripe Tax Does

ResponsibilityDescription
Tax jurisdiction detectionDetermines which country/state/province tax rules apply based on customer location
Tax rate calculationCalculates the correct rate for the jurisdiction and product type
VAT number validationValidates EU VAT numbers against the VIES database
Reverse charge detectionIdentifies B2B intra-EU transactions where reverse charge applies
Global coverageSupports tax calculation for 100+ countries

What You Must Handle

ResponsibilityDescription
Invoice generationCreating, storing, and serving invoice documents (Stripe handles this in stripe_managed mode)
Invoice archivingLegal retention of invoices (configurable via INVOICE_RETENTION_YEARS, default: 10 years)
Tax reportingFiling VAT returns, sales tax reports, and other tax declarations
Legal mentionsAdding required legal text to invoices (e.g., reverse charge mention, VAT exemption text)
Tax registrationRegistering for VAT/GST in relevant jurisdictions
Stripe Tax calculates taxes for you, but it does not file your tax returns or handle tax remittance. You need a tax advisor or compliance service for reporting obligations.

Enabling Stripe Tax

Stripe Tax is an opt-in feature controlled by an environment variable:

backend/.env
STRIPE_TAX_ENABLED=true

This maps to the billing configuration:

backend/config/docs/billing.php
'tax' => [
    'provider' => env('TAX_PROVIDER', 'stripe_tax'),

    'stripe_tax' => [
        'enabled' => env('STRIPE_TAX_ENABLED', false),
    ],
],

Setup Steps

  1. Enable Stripe Tax in your Stripe Dashboard (Settings > Tax)
  2. Configure your tax registration — tell Stripe where you are registered to collect tax
  3. Set product tax codes — assign Stripe tax codes to your products (e.g., txcd_10000000 for software)
  4. Set the environment variableSTRIPE_TAX_ENABLED=true in your .env

When enabled, tax calculations are automatically applied to Checkout Sessions and invoices. When disabled, no tax is calculated and all tax amounts are zero.


The TaxProvider Abstraction

Tax calculation is abstracted behind the TaxProviderInterface, allowing you to swap providers without changing business logic:

backend/app/Domain/Billing/Contracts/TaxProviderInterface.php
interface TaxProviderInterface
{
    /**
     * Calculate tax for a given amount and context.
     * Tax is calculated in the same currency as the amount.
     */
    public function calculate(Money $amount, TaxContext $context): TaxCalculation;

    /**
     * Validate a VAT number with the tax authority.
     */
    public function validateVatNumber(string $vatNumber, string $countryCode): bool;

    /**
     * Determine if reverse charge applies for B2B transactions.
     */
    public function isReverseCharge(TaxContext $context): bool;

    /**
     * Get the standard tax rate for a jurisdiction.
     *
     * @param  string  $countryCode  ISO 3166-1 alpha-2 country code
     * @param  string|null  $region  State/province for US, CA, etc.
     * @return float Tax rate as decimal (e.g., 0.20 for 20%)
     */
    public function getTaxRate(string $countryCode, ?string $region = null): float;
}

The default implementation is StripeTaxProvider (backend/app/Infrastructure/Billing/Providers/Stripe/StripeTaxProvider.php), bound in the BillingServiceProvider.


Tax Context and Calculation

Tax calculations require a context describing the customer and transaction:

TaxContext DTO

backend/app/Domain/Billing/DTO/TaxContext.php
final readonly class TaxContext
{
    public function __construct(
        public string $customerCountry,        // ISO 3166-1 alpha-2 (e.g., 'FR', 'US')
        public ?string $customerPostalCode = null,
        public ?string $customerState = null,   // For US, CA, AU, etc.
        public ?string $vatNumber = null,        // EU VAT number for B2B
        public ?string $taxCode = null,          // Stripe product tax code
        public string $reference = 'subscription',
    ) {}
}

TaxCalculation DTO

The calculation result contains the full tax breakdown:

backend/app/Domain/Billing/DTO/TaxCalculation.php
final readonly class TaxCalculation
{
    public function __construct(
        public Money $taxableAmount,           // Amount before tax
        public Money $taxAmount,               // Calculated tax
        public Money $totalWithTax,            // Amount + tax
        public float $rate,                    // Rate as decimal (0.20 = 20%)
        public string $jurisdiction,           // Tax jurisdiction code
        public string $taxType,                // e.g., 'vat', 'sales_tax', 'gst'
        public bool $isReverseCharge,          // true for B2B intra-EU
        public ?string $providerCalculationId = null,  // Stripe calculation ID
    ) {}
}

All money amounts in the calculation use the same currency — cross-currency tax calculations are not supported. See Currency Rules.


VAT Number Validation

The StripeTaxProvider validates EU VAT number formats locally before calling Stripe's validation API. This prevents unnecessary API calls for obviously invalid numbers.

Supported Countries

VAT format validation covers all 27 EU member states:

CountryCodeVAT Pattern Example
AustriaATATU12345678
BelgiumBEBE0123456789
FranceFRFR12345678901
GermanyDEDE123456789
ItalyITIT12345678901
NetherlandsNLNL123456789B01
SpainESESA12345678
......(all 27 EU countries)

Tax ID Type Mapping

When sending tax IDs to Stripe, the provider maps country codes to Stripe's tax ID types:

RegionTax ID Type
EU countrieseu_vat
United Kingdomgb_vat
Switzerlandch_vat
Norwayno_vat
Australiaau_abn
New Zealandnz_gst
Canadaca_bn
United Statesus_ein

Reverse Charge Detection

For B2B transactions within the EU, the provider automatically detects reverse charge eligibility:

  • The customer must have a valid EU VAT number
  • The customer must be in a different EU country than the seller
  • When reverse charge applies, the tax rate is 0% and the invoice must include a reverse charge mention
// Check reverse charge for a B2B transaction
$context = new TaxContext(
    customerCountry: 'DE',
    vatNumber: 'DE123456789',
);

$isReverseCharge = $taxProvider->isReverseCharge($context);
// true if your company is registered in a different EU country (e.g., FR)

Tax Records on Invoices

When invoices are synced from Stripe via webhooks, tax breakdown data is mapped to local TaxRecord entries using the TaxBreakdownMapper:

backend/app/Infrastructure/Billing/Mappers/TaxBreakdownMapper.php
final class TaxBreakdownMapper
{
    /**
     * Map Stripe invoice tax breakdown to TaxRecordDraft array.
     *
     * @return TaxRecordDraft[]
     */
    public function map(StripeObject $stripeInvoice): array
}

Each TaxRecordDraft captures:

backend/app/Domain/Billing/DTO/TaxRecordDraft.php
final readonly class TaxRecordDraft
{
    public function __construct(
        public string $taxType,             // 'vat', 'sales_tax', 'gst'
        public string $jurisdiction,         // Country/region code
        public float $rate,                  // Decimal rate (0.200000)
        public int $taxableAmountCents,      // Taxable base in minor units
        public int $taxAmountCents,          // Tax amount in minor units
        public ?string $stripeCalculationId = null,
    ) {}
}

Tax records are stored alongside invoice data, providing a complete audit trail of how taxes were calculated for each invoice.


Tax Rate Lookup

The getTaxRate() method provides indicative standard tax rates for any jurisdiction. Results are cached for 24 hours:

// Get the standard VAT rate for France
$rate = $taxProvider->getTaxRate('FR');
// 0.20 (20%)

// Get the sales tax rate for California
$rate = $taxProvider->getTaxRate('US', 'CA');
// Rate varies by locality
These rates are indicative and used for estimation purposes (e.g., displaying estimated tax on a pricing page). Actual tax amounts on invoices are always calculated by Stripe Tax at transaction time, which accounts for product tax codes, customer exemptions, and jurisdiction-specific rules.

Company Billing Information

Your company's billing details are configured via environment variables and used on invoices:

backend/.env
BILLING_COMPANY_NAME=Your Company
BILLING_COMPANY_ADDRESS=123 Main Street
BILLING_COMPANY_CITY=Paris
BILLING_COMPANY_POSTAL_CODE=75001
BILLING_COMPANY_COUNTRY=FR
BILLING_COMPANY_VAT_NUMBER=FR12345678901

These values are read from config/docs/billing.php:

backend/config/docs/billing.php
'company' => [
    'name' => env('BILLING_COMPANY_NAME', 'Your Company'),
    'address' => env('BILLING_COMPANY_ADDRESS', ''),
    'city' => env('BILLING_COMPANY_CITY', ''),
    'postal_code' => env('BILLING_COMPANY_POSTAL_CODE', ''),
    'country' => env('BILLING_COMPANY_COUNTRY', 'FR'),
    'vat_number' => env('BILLING_COMPANY_VAT_NUMBER', ''),
],

The company country is also used by the StripeTaxProvider to determine whether a transaction is domestic or cross-border (relevant for EU reverse charge).


Invoice retention is configurable for compliance with local regulations:

backend/config/docs/billing.php
'invoices' => [
    'retention_years' => env('INVOICE_RETENTION_YEARS', 10),
    'storage_disk' => env('INVOICE_STORAGE_DISK', 'local'),
    'legal_mentions' => [
        'default' => env('INVOICE_LEGAL_MENTION', ''),
    ],
],

Common retention requirements by jurisdiction:

CountryMinimum Retention
France10 years
Germany10 years
United Kingdom6 years
United States3-7 years (varies by state)
Canada6 years
These retention periods are provided as general guidance. Consult a tax advisor for your specific obligations based on your business structure and registration jurisdictions.

Extending the Tax Provider

To implement a custom tax provider (e.g., for a different tax calculation service), create a class implementing TaxProviderInterface and bind it in a service provider:

// In a service provider
$this->app->bind(
    TaxProviderInterface::class,
    MyCustomTaxProvider::class
);

Your implementation must handle all four methods: calculate(), validateVatNumber(), isReverseCharge(), and getTaxRate(). The rest of the billing system depends only on the interface, so no other changes are needed.


V1 Limitations

LimitationRationale
No dynamic tax overridesTax rates are fully determined by Stripe Tax. Manual rate overrides are not supported.
No manual tax exemptionsTax exemptions must be configured in Stripe Dashboard, not via the API.
Stripe Tax onlyThe TaxProviderInterface exists for extensibility, but only StripeTaxProvider is shipped.

What's Next

  • Webhooks — How webhook events sync tax data from Stripe to your application
  • Invoices — Where tax records appear on invoice data
  • Currency Rules — How tax amounts follow the same currency as the invoice