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). The movements behind a credit balance are kept the same way, because each one is a booking record that a booking batch built after the erasure still has to contain. Everything else goes: subscriptions, usage, credit balances and the personal data inside the webhook payloads. The package stores no provider API keys; if your app does, erasing them is yours, and BillableAccountDeleting below is where it belongs.

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.

A row in which the person only acted is somebody else's record. Each audit row names whom it is about and who acted. When your support staff sign in with the same model as your customers, a refund one of them made to a customer names the customer as its subject and the staff member as its actor. billing:erase deletes the rows about the person and keeps the rows in which they only acted, with the actor cleared: the customer's history keeps the refund, and nothing on it names the erased person. billing:export hands the person those rows as well, as what they did, but without whom it was done to and what the row records about them (Art. 15(4)): the subject and the payload are left empty.

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();

Replaying erasures after a restore touches only the records. When you restore a backup and replay the erasures made since, the provider has already ended those subscriptions and, where configured, forgotten the customer, while the restored rows show them as running again. BillingEraser::eraseRecords($user) erases what the package holds and reaches nothing at the provider: no BillableAccountDeleting, no cancellation, no request to forget the customer. It returns the same ErasureReport as erase(), which runs it as its middle step.

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