Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for VAT ID on Billable #841

Closed
lsmith77 opened this issue Dec 24, 2019 · 4 comments
Closed

add support for VAT ID on Billable #841

lsmith77 opened this issue Dec 24, 2019 · 4 comments

Comments

@lsmith77
Copy link

related to #830

Stripe supports setting customer Tax IDs https://stripe.com/docs/billing/taxes/tax-ids
This is useful for the following purposes:

  • adding the Tax ID to invoices, which is a legal requirement in many countries
  • tracking the status of the Tax ID
  • determining tax status (ie. except or not)

It is a MUST to set the Tax ID for B2B cases. Therefore it IMHO would make sense to add native support into the Billable class (less code to write, automate setting the tax exemption status) and provides a clear API that packages like https://github.com/mpociot/vat-calculator can target to add additional features.

Here is some code I have added in my current project to make it work without cashier support. It would of course be nice to support more different country formats of course.

try {
    if ($country !== substr($vat_id, 0, 2)) {
        throw new \Exception('Country does not match VAT ID');
    }

    Stripe::setApiKey(config('services.stripe.secret'));

    /** @var TaxId[] $taxIds */
    $taxIds = Customer::allTaxIds(
        $billable->stripe_id,
        ['limit' => 3]
    );

    $vatIdChanged = true;
    foreach ($taxIds as $taxId) {
        if ($taxId->value !== $vat_id) {
            Customer::deleteTaxId(
                $billable->stripe_id,
                $taxId->id
            );
        } else {
            $vatIdChanged = false;
        }
    }

    if ($vatIdChanged) {
        Customer::createTaxId(
            $billable->stripe_id,
            [
                'type' => $country === 'CH' ? 'ch_vat' : 'eu_vat',
                'value' => $vat_id
            ]
        );
    }

    if ($user->country !== Spark::homeCountry()) {
        $stripeCustomer->tax_exempt = 'exempt';
    } else {
        $stripeCustomer->tax_exempt = 'none';
    }

     $stripeCustomer->save();
} catch (\Stripe\Error\InvalidRequest | \Stripe\Exception\InvalidRequest | \Exception $e) {
    throw ValidationException::withMessages([
        'vat_id' => Lang::get('validation.vat_id'),
    ]);
}

FYI the Stripe Tax ID API is not very mature yet, ie. its not possible to fetch a Tax ID object by VAT ID and more importantly it is not possible to update a Tax ID status, which is actually quite critical, since the company status needs to be validated regularly, to for example determine if the customer is in fact tax exempt or not.

I created a simple command to validate the Tax ID status over night, since this depends on government API's that are not really reliable:

<?php

namespace App\Console\Commands;

use App\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use Laravel\Spark\Spark;
use Mpociot\VatCalculator\Exceptions\VATCheckUnavailableException;
use Mpociot\VatCalculator\VatCalculator;
use Stripe\Customer;
use Stripe\Stripe;
use Stripe\TaxId;

class ValidateVAT extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'validate:vat {--dump=}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Validate VAT ID of all customers along with their address';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $vatCalculator = new VatCalculator();
        $vatCalculator->setBusinessCountryCode(Spark::homeCountry());

        $users = User::query()
            ->whereNotNull('vat_id')
            ->where('billing_country', '!=', Spark::homeCountry())
            ->whereNotNull('stripe_id')
            ->get();

        $userCount = count($users);
        $this->info("Read $userCount users VAT ID");

        Stripe::setApiKey(config('services.stripe.secret'));

        $errorCount = 0;
        $validations = [];
        foreach ($users as $user) {
            try {
                $errors = [];
                $stripeTaxId = null;

                $details = $vatCalculator->getVATDetails($user->vat_id);

                if (!$details || !$details->valid) {
                    $errors[] = "VAT '{$user->vat_id}' not valid";
                } else {
                    if ($details->countryCode !== $user->billing_country) {
                        $errors[] = "Billing country expected '{$details->countryCode}'";
                    }
                    if ($details->name !== strtoupper($user->billing_company)) {
                        $errors[] = "Billing company expected '{$details->name}'";
                    }
                }

                try {
                    $taxIds = Customer::allTaxIds(
                        $user->stripe_id,
                        ['limit' => 3]
                    );

                    foreach ($taxIds as $taxId) {
                        if ($taxId->value === $user->vat_id) {
                            $stripeTaxId = $taxId;
                            break;
                        }
                    }
                } catch (\Stripe\Error\InvalidRequest | \Stripe\Exception\InvalidRequest $e) {
                    $errors[] = "Unable to find VAT on stripe for stripe customer '{$user->stripe_id}'";
                }

                $result = [
                    "Validated VAT ID '{$user->vat_id}':",
                    config('app.url')."/spark/kiosk#/users/".$user->id,
                    "country '{$user->billing_country}' and company name '{$user->billing_company}'",
                ];

                if (empty($errors)) {
                    $result[] = 'Valid';

                    $stripeVerification = [
                        'status' => TaxId::VERIFICATION_STATUS_VERIFIED,
                        'verified_name' => true,
                    ];
                } else {
                    ++$errorCount;

                    $stripeVerification = [
                        'status' => TaxId::VERIFICATION_STATUS_UNVERIFIED,
                        'verified_name' => null,
                    ];

                    $result = array_merge($result, $errors);
                }

                $validations[] = implode("\n", $result);
            } catch(VATCheckUnavailableException $e ){
                ++$errorCount;

                $stripeVerification = [
                    'status' => TaxId::VERIFICATION_STATUS_UNAVAILABLE,
                    'verified_name' => null,
                ];

                $validations[] = 'Validation service failed';
            }

            try {
                if ($stripeTaxId
                    && $stripeTaxId->verification['status'] !== $stripeVerification['status']
                ) {
                    Customer::deleteTaxId(
                        $user->stripe_id,
                        $taxId->id
                    );

                    Customer::createTaxId(
                        $user->stripe_id,
                        [
                            'type' => 'eu_vat',
                            'value' => $user->vat_id,
                            'verification' => $stripeVerification,
                        ]
                    );
                }
            } catch (\Stripe\Error\InvalidRequest | \Stripe\Exception\InvalidRequest $e) {
            }
        }

        $message = implode("\n\n", $validations);

        $dump = $this->option('dump');
        if ($dump) {
            $this->info($message);
            return;
        }

        $data = [
            'from' => Spark::supportAddress(),
            'subject' => "VAT Report with $errorCount Errors founds",
            'message' => $message,
        ];

        Mail::raw($data['message'], function ($m) use ($data) {
            $m->to(Spark::supportAddress())->subject(__('Publish Request: ').$data['subject']);

            $m->replyTo($data['from']);
        });

        $this->info("Massage send to: ".Spark::supportAddress());
    }
}
@lsmith77 lsmith77 mentioned this issue Dec 24, 2019
3 tasks
@lsmith77
Copy link
Author

@mpociot FYI ^

@driesvints
Copy link
Member

driesvints commented Dec 24, 2019

Thanks, I'll try to have a look at this when I'm back from vacation in two weeks.

@driesvints
Copy link
Member

@lsmith77 I only see CRUD operations in your examples and no real other use cases. There's lots of other CRUD operations that aren't directly integrated into Cashier and which you can simply use the Stripe SDK for. So I don't think it's necessary to add anything to Cashier as most of it will only be minimal syntactic sugar on top of the Stripe SDK.

I've btw already added methods to the Billable trait to easily determine if a customer is tax exempt or not.

@lsmith77
Copy link
Author

lsmith77 commented Jul 6, 2020

FYI Stripe now supports validation for some countries. If the status changes there is a webhook that is triggered https://stripe.com/docs/billing/taxes/tax-ids?lang=php#validation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants