Skip to main content

Data protection

The package stores personal data on your behalf — the customer's name and address on an invoice, and the raw provider webhook payloads, which carry their email, name, billing address and the last four digits of their card. So it ships the two things a GDPR request actually needs.

php artisan billing:export 42 # everything we hold about owner 42, as JSON (Art. 15 / Art. 20)
php artisan billing:erase 42 # erase it (Art. 17)
php artisan billing:erase 42 --dry-run # …or see first what would go and what would stay

billing:erase deliberately does not delete the invoices. A valid invoice has to carry the buyer's name and address (§14 UStG), and invoices have to be kept for years (§147 AO, §14b UStG) — the right to erasure yields to a legal retention obligation (Art. 17(3)(b)). Those rows are unlinked from the owner and kept, and billing:prune removes them once the retention window closes (billing.retention.erased_financial_days, default eight years — check it against your own jurisdiction). Everything else goes: subscriptions, usage, credit balances, the owner's own stored provider API keys, and the personal data inside the webhook payloads.

A credit balance is money you still owed the customer, so it is written to the audit ledger before it is purged rather than vanishing quietly.

billing.erasure.forget_customer additionally DELETES the customer at the provider. That is irreversible and it cancels their live subscriptions there, so it is off by default — turn it on deliberately. Stripe keeps its own invoice and charge records regardless.

The command is the complete path. An observer on your User model would look more convenient, but a mass delete (User::query()->where(...)->delete()) fires no model events at all — an app relying on one would under-erase and never know. Call billing:erase (or the BillingEraser) from wherever you handle the request.

Deleting an account stops live billing first. A deleted owner whose subscription keeps charging is a money leak (and billing someone you erased is a compliance breach). billing:erase / BillingEraser fire a BillableAccountDeleting event before they erase, and the package cancels the owner's subscription immediately (not into a grace period) in response — tolerant of a provider blip, which is logged and lets the delete continue rather than leaving a user who asked to leave undeletable. If your app has its own delete UI that does not go through BillingEraser, dispatch the event yourself, right after re-confirming identity and before $user->delete():

use Pushery\Billing\Events\BillableAccountDeleting;

event(new BillableAccountDeleting($user)); // cancels billing now
$user->delete();

billing:prune also ages out the stored webhook payloads on its own clock (billing.retention.webhook_payload_days, default 90). They exist for exactly one reason — so a failed effect can be re-driven from what the provider already sent — and the provider itself stops redelivering after about three days. A payload whose effects are still owed is never pruned, however old it is.

The privacy notice for place-of-supply evidence

The package determines which country a supply is taxed in and stores that country against the sale, because tax law requires the evidence to be retained. That is processing you have to disclose, and the disclosure is yours to publish — it belongs on your privacy page, in your layout, alongside everything else you process.

What the package ships is the wording, translated into all seven locales, under billing::privacy:

KeyWhat it covers
privacy.place_of_supply.headingThe section heading
privacy.place_of_supply.purposeWhy the country is determined and recorded
privacy.place_of_supply.legal_basisThe legal obligation it rests on — explicitly not consent, and not a balancing of interests
privacy.place_of_supply.data_categoriesWhat is stored: the two-letter country code, and the network address where one is used to derive it
privacy.place_of_supply.retentionHow long it is kept, and why that period
privacy.place_of_supply.no_consent_noteWhy no consent banner applies to it

Render them wherever your privacy page lives:

<h2>{{ __('billing::privacy.place_of_supply.heading') }}</h2>
<p>{{ __('billing::privacy.place_of_supply.purpose') }}</p>
<p>{{ __('billing::privacy.place_of_supply.legal_basis') }}</p>
<p>{{ __('billing::privacy.place_of_supply.data_categories') }}</p>
<p>{{ __('billing::privacy.place_of_supply.retention') }}</p>
<p>{{ __('billing::privacy.place_of_supply.no_consent_note') }}</p>

The split is deliberate: getting this wording right in seven languages is the hard part and it is the same for every shop, while the page it sits on is yours and is not. Publish php artisan vendor:publish --tag=billing-lang if you need to adjust the text — the retention period in particular states ten years, which is the German requirement the shipped wording was written against.

For the full retention and deletion matrix, see Retention and erasure.


← Back to the documentation index