1

I need to apply taxes to subscriptions. How do I do that? The idea is that prices are first shown without taxes. Then – on the payment page – I need some kind of "bill" that shows all the relevant infos and charges the correct price (subscription plus taxes):

Monthly Subscription    5€
VAT                  0,95€
--------------------------
SUM                  5,95€

Is that even possible with commerce-stripe at the moment?

Cheers, Mark

8

I realise this is a bit of an old question, but I was faced with the same problem recently so I thought I'd document my solution here for anyone in the future that comes along needing to apply tax to a subscription in Craft Commerce.

There are two problems here:

  1. Subscriptions aren't purchasables in Commerce at the moment, so they can't be added to a cart and have tax rates applied to them in the same way that you would with 'normal' products.
  2. Subscription pricing is determined by Stripe rather than internally in Commerce, and the same is true of tax rates. There's no way of syncing or importing tax rates defined in Stripe into Commerce.

As far as I know, there is no out-of-the-box method in Commerce to apply tax to subscriptions automatically. It requires either manually adjusting subscription invoices within Stripe, or programatically adding a tax rate to a subscription before it's sent to Stripe. I chose the latter method.

Showing a bill before payment

I solved this by creating a single 'Subscription' product within Commerce, and then creating a number of variants on this product to represent the various subscriptions that I had defined in Stripe, including setting the prices to match those that are defined in Stripe. I added a custom field called subscriptionPlanHandle to the variant config to hold the handle of the subscription plan (handles for subscriptions are found in Commerce > System Settings > Subscription Plans), to allow me to fetch the correct subscription plan when creating the payment page.

Next, I set up a tax rate in Commerce, in my case VAT. I then set this tax rate to be applied to my subscription products - this can be done in Commerce > System Settings > Product Types and then edit the product type to set its Available Tax Categories (under the Tax & Shipping tab) to the relevant tax rate.

I then used the standard 'cart' approach to my purchase journey, showing my subscription products on a product page and allowing one to be added to the cart. A summary of the pricing was shown by looping through the contents of the cart and showing line item pricing followed by the tax adjustment and then a total:

{% set cart = craft.commerce.carts.cart %}

{% for item in cart.lineItems %}
    {% set itemsSubtotal = itemsSubtotal + item.subtotal %}

    <p>{{ item.description }}</p>
    <p>{{ item.subtotal|commerceCurrency(cart.currency, stripZeros=true) }}</p>
{% endfor %}

{% set totalPrice = itemsSubtotal %}

{% if cart.adjustments|length %}
    <h3>Total</h3>

    {% for adjustment in cart.orderAdjustments %}
        {% set totalPrice = totalPrice + adjustment.amount %}

        <p>Excl. {{ adjustment.name }}: {{ itemsSubtotal|commerceCurrency(cart.currency, stripZeros=true) }}<p>
        <p><strong>Incl. {{ adjustment.name }}: {{ totalPrice|commerceCurrency(cart.currency, stripZeros=true) }}</strong></p>
    {% endfor %}
{% else %}
    <p>{{ totalPrice|commerceCurrency(cart.currency) }}</p>
{% endif %}

Now, since subscriptions aren't purchasables in Commerce (yet), you can't just go through the normal checkout since the subscriptions won't be sent to Stripe. On the payment page/section, you have to do some juggling about to get the subscription plan UID from the product(s) in the cart and then create the appropriate payment form. Here's how I solved it:

<form method="post" accept-charset="UTF-8" novalidate>

    {{ csrfInput() }}
    {{ actionInput('commerce/subscriptions/subscribe') }}

    {% for item in cart.lineItems %}
        {% set variant = craft.variants().id(item.purchasableId).one() %}
        {% set plan = craft.commerce.getPlans().getPlanByHandle(variant.subscriptionPlanHandle) %}

        {{ plan.getGateway().getPaymentFormHtml({})|raw }}

        <input type="hidden" name="planUid" value="{{ plan.uid|hash }}">
    {% endfor %}

    <button type="submit">Pay</button>

</form>

This fetches the variant using the the item.purchasableId property, and then fetches the matching subscription plan using the custom subscriptionPlanHandle field mentioned above. A hidden field is then added to the form with the value of the plan's UID. The form can then be submitted to the subscription controller to set up the subscription.

Applying tax to a subscription

My solution requires either a custom module or plugin, so that you can listen to the EVENT_BEFORE_SUBSCRIBE event triggered by the Stripe gateway and modify the individual subscription item to add a tax code.

The steps to get this work are as follows:

  1. In your Stripe account, you need to define a tax rate. You can do this by going to Billing > Tax rates in your Stripe dashboard, clicking 'New' and creating a new tax rate. In my case, I needed to apply VAT so I created a tax with a rate of 20%, named it 'VAT' and saved it. Next, you need to grab the ID of your new tax rate. Copy this to your clipboard - you'll need it in the next step.
  2. Back in Commerce, find the appropriate tax rate in Commerce > Tax > Tax Rates. Paste the Stripe tax ID into the 'Code' field in Commerce. In my case, I turned on the 'Don’t apply this tax when the address has a valid VAT ID' option, as a simple way to identify that this rate was a VAT rate.
  3. In your custom module or plugin, add an event handler in the main plugin file. This can either be directly in the init method or you could have a separate method to apply all of your event handlers that you invoke from the init method.

Here's the event handler code:

Event::on(
    StripeGateway::class,
    StripeGateway::EVENT_BEFORE_SUBSCRIBE,
    function(SubscriptionRequestEvent $event) {
        $taxRates = Commerce::getInstance()->getTaxRates()->getAllTaxRates();

        foreach ($taxRates as $taxRate) {
            if ($taxRate->isVat) {
                $event->parameters['items'][0]['tax_rates'] = [$taxRate->code];
            }
        }
    }
);

This event is triggered before a subscription is sent to Stripe, and allows you to modify the parameters that are sent to Stripe - like, say, to add in a tax rate!

The code inside the event handler gets all of the tax rates, then loops through each one until it finds one whose isVat property is set. It then adds a new tax_rates property to the first item in the items array (which should only have one property beforehand - the planId), and sets its value to an array containing the value of the code property from the tax rate.

Using the code property to hold the ID of the Stripe tax rate allows this data to be content-managed in Commerce, so that you can swap between test data and live data easily without requiring a code change. I'm not sure if there's another use for the code property within Commerce, but this worked for me.

This method is reasonably robust if you only need to apply a single tax rate like VAT, but if you need to charge lots of different tax rates it would need some modification to ensure the right tax rate is applied.

Note: these code examples don't deal with determining whether the VAT rate should be applied; you'll need to make sure that the VAT is only applied in the appropriate countries depending on the country set in the customer's shipping address.

Tidying up afterwards

This process should hopefully leave you with an active subscription in Stripe with the correct tax rate applied, but also an unfinished order since the subscription controller doesn't use the cart at all. I chose to handle this by listening to the EVENT_AFTER_CREATE_SUBSCRIPTION event and deleting the cart directly once the subscription was created.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.