Documentation for larsasub v0.38.2.
On an older version? Your release notes ship with the package (CHANGELOG.md).
Database schema
Every table the package creates, column by column. Use it when you write a report or a query against LarsaSub's tables, or when you need to check a development database against what the current release expects.
There are twenty-seven tables. All of them carry the larsasub.table_prefix
(default larsasub_) and live on the larsasub.database.connection
connection (default: your application's default connection). The published
migrations are the authority — run
php artisan vendor:publish --tag=larsasub-migrations to read them.
- Conventions
- Core billing
- Payments
- Credit
- Invoicing
- Provider (Mollie)
- Supporting
- Keeping the schema in step
Conventions
The column tables below leave out what every table shares, so each row is a column that tells you something.
idand timestamps. Every table starts with an auto-incrementing unsigned big-integeridand ends withcreated_at/updated_at(nullable timestamps). The one exception islarsasub_model_logs, which hascreated_atonly.- Types are the Laravel schema-builder methods the migration calls.
stringwithout a length isstring(255). Amounts aredecimal(10,2)and VAT ratesdecimal(5,2)(a percentage:21.00). - Morph pairs (
tenant,provider_mandate,invoiceable,source,entity,loggable) are a{name}_typestring plus a{name}_idunsigned big integer, indexed together. - Tenant columns.
tenant_type/tenant_idname the Connect tenant a row belongs to. Null means no tenant: simple mode, or the platform's own account. - Foreign keys restrict deletion unless the table says otherwise.
- Currency defaults (
price_currency,currency) takecurrency.defaultas it stands when the migration runs. - Columns are contract. The model columns are part of the public API: removing one is a breaking change.
Seven tables use soft deletes (deleted_at), so the audit trail survives a
cleanup: larsasub_subscriptions, larsasub_billable_subscriptions,
larsasub_subscription_cost_lines, larsasub_recurring_charges,
larsasub_single_payments, larsasub_mollie_customers and
larsasub_mollie_mandates.
These columns are encrypted at rest through model casts, so they hold ciphertext — never query or compare them in SQL:
| Table | Columns | Cast |
|---|---|---|
larsasub_recurring_charges, larsasub_single_payments |
vat_customer, provider_payment_details, provider_chargeback_details, provider_refund_details |
encrypted:array |
larsasub_mollie_mandates |
consumer_account |
encrypted |
larsasub_mollie_mandates |
details |
encrypted:array |
larsasub_mollie_connected_accounts |
access_token, refresh_token |
encrypted |
larsasub_connect_flow_states |
code |
encrypted |
An encrypted array sits on a longText column, not json: a ciphertext is not
valid JSON, and MySQL rejects it in a JSON column.
Core billing
The registration, the plans and each subscriber's copy of a plan.
larsasub_billables
The registration linking your owner model (User, Team, Tenant) to everything
else. Model: Billable — see Billable models.
| Column | Type | Null | Notes |
|---|---|---|---|
profile_code |
string(24) | no | Unique. Eight random characters, prefixed with the tenant's reference code in Connect mode; shown on the dashboards |
owner_id |
unsignedBigInteger | no | Your owner model's key |
owner_type |
string | no | Your owner model's morph class |
tenant_type |
string | yes | The Connect tenant |
tenant_id |
unsignedBigInteger | yes | The Connect tenant |
tenant_key |
string(191) | no | Default ''. Non-null mirror of the tenant, {morph class}:{id}, '' without a tenant |
locale |
string(12) | yes | The billable's content locale. Money paths read this, never the owner |
invoice_type |
string(16) | no | Default personal; personal or business. See The invoice profile |
invoice_first_name |
string(128) | yes | |
invoice_last_name |
string(128) | yes | |
invoice_email |
string(255) | yes | |
invoice_company_name |
string(255) | yes | |
invoice_vat_number |
string(32) | yes | |
invoice_coc_number |
string(32) | yes | Chamber of commerce number |
invoice_address_line_1 |
string(255) | yes | |
invoice_address_line_2 |
string(255) | yes | |
invoice_city |
string(128) | yes | |
invoice_postal_code |
string(16) | yes | |
invoice_state |
string(128) | yes | |
invoice_country |
string(2) | yes | ISO 3166-1 alpha-2 code |
vat_check_status |
string(16) | yes | Enums\VatNumberStatus value of the VIES check; null = not applicable or never checked. See Validating VAT numbers |
vat_check_number |
string(34) | yes | The normalized number the status belongs to (VIES country code + national number) |
vat_check_reference |
string(255) | yes | VIES consultation number of the last valid answer |
vat_checked_at |
timestamp | yes | Date of the last definitive VIES answer |
anonymized_at |
timestamp | yes | Set by Billable::anonymize(). See Erasing a billable |
The four vat_check_* columns are written by the package only and are not
fillable, so a profile form cannot set them.
Indexes:
- Unique
(owner_type, owner_id, tenant_key)— one registration per owner per tenant.tenant_keyis non-null precisely so this works: NULLs are never equal in a unique index, and customer #1 of two different tenants must be two registrations. (tenant_type, tenant_id)and(invoice_type, vat_check_status).
larsasub_subscriptions
Plan templates. Model: Subscription — see
The subscription plan. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
tenant_id |
unsignedBigInteger | yes | Null = a global plan |
tenant_type |
string | yes | |
feature_list |
string(32) | yes | |
features |
json | yes | |
name |
text | no | Translatable: a plain string or a JSON locale map. See Multilingual plan content |
description |
text | yes | Translatable |
content |
mediumText | yes | Translatable |
active |
boolean | no | Default false |
start_date |
date | yes | Available from |
end_date |
date | yes | Available until |
duration |
string(12) | no | The contract term |
billing_interval_months |
unsignedTinyInteger | no | Default 1. Whole months, 1–12; a fixed duration must hold a whole number of periods (validated on the model) |
is_charged |
boolean | no | Default true |
price_amount |
decimal(10,2) | no | The price per billing period, not per month |
price_currency |
string(6) | no | Default currency.default |
vat_rate |
decimal(5,2) | yes | Per-plan VAT rate; null = invoicing.vat_rate. Every subscription without an override of its own follows it from its next charge; a change through the model fires SubscriptionVatRateChangedEvent |
introduction_duration |
string(12) | no | |
introduction_price_amount |
decimal(10,2) | no | |
cancellation_notice_period |
string(12) | no | |
order_nr |
integer | yes | Sort order |
internal_note |
string(510) | yes | |
automatic_renewal |
boolean | no | Default false |
renewal_cancellation_notice_period |
string(12) | no | |
fee_type |
string(16) | yes | Connect application fee override: percentage or fixed; null = connect.fee |
fee_value |
string(10) | yes | 5 (5%) or 0.25 (€0.25) |
minimum_days_before_change |
unsignedInteger | yes | Null = subscription.minimum_days_before_change |
deleted_at |
timestamp | yes | Soft delete |
Indexes: (tenant_id, tenant_type).
larsasub_subscription_change_targets
Which plans a plan may be switched to. A pivot with no model of its own; it
backs Subscription::changeTargets().
| Column | Type | Null | Notes |
|---|---|---|---|
subscription_id |
unsignedBigInteger | no | FK larsasub_subscriptions, cascade on delete |
target_subscription_id |
unsignedBigInteger | no | FK larsasub_subscriptions, cascade on delete |
Indexes: unique (subscription_id, target_subscription_id).
larsasub_billable_subscriptions
One subscriber's instance of a plan. The price and terms are copied from the
plan at creation, so a later plan edit never changes the price a running
subscriber pays or how often. The VAT rate is the exception: it is not copied,
and follows the plan (see vat_rate). Model: BillableSubscription — see
The BillableSubscription instance.
Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
subscription_id |
unsignedBigInteger | no | FK larsasub_subscriptions — the plan |
billable_id |
unsignedBigInteger | no | FK larsasub_billables |
payment_method_id |
unsignedBigInteger | no | FK larsasub_payment_methods |
provider_mandate_type, provider_mandate_id |
morph | yes | The mandate this subscription collects with |
activated_at |
timestamp | yes | |
deactivated_at |
timestamp | yes | |
activated_status |
string(64) | no | Display and audit text. Never query it — key on the two enum columns below |
deactivation_reason |
string(48) | yes | Enums\DeactivationReason value |
activation_source |
string(48) | yes | Enums\ActivationSource value; null on host-forced activations without a known source |
start_date |
date | no | The real start; anchors the billing day and the proration. Change it through BillableSubscriptionService::moveStartDate() — see Moving the start date |
end_date |
date | yes | |
first_charge_on |
date | yes | The first day the subscription may be collected on; null = bill from start_date. Set when the period before it was paid elsewhere, so an imported subscriber is not charged twice. See Migrating subscribers in |
duration |
string(12) | no | Copied from the plan |
billing_interval_months |
unsignedTinyInteger | no | Default 1. Copied from the plan |
is_charged |
boolean | no | Default true |
price_amount |
decimal(10,2) | no | Per billing period |
price_currency |
string(6) | no | Default currency.default |
vat_rate |
decimal(5,2) | yes | An override for this subscription; null = the plan's rate (a soft-deleted plan's too), then invoicing.vat_rate, read when each charge is composed. Never copied from the plan |
introduction_duration |
string(12) | no | |
introduction_price_amount |
decimal(10,2) | no | |
cancellation_notice_period |
string(12) | no | |
automatic_renewal |
boolean | no | Default false |
renewal_cancellation_notice_period |
string(12) | no | |
name |
text | no | Copied raw from the plan, full locale map included |
description |
text | yes | Copied raw from the plan |
feature_overrides |
json | yes | |
canceled_at |
timestamp | yes | |
credit_amount |
decimal(10,2) | yes | |
renewal_announced_at |
timestamp | yes | |
started_processed_at |
timestamp | yes | Set once the upcoming-to-active transition was handled |
deleted_at |
timestamp | yes | Soft delete |
Indexes: billable_id; (started_processed_at, start_date); the
provider_mandate morph pair.
larsasub_subscription_cost_lines
Recurring add-ons on a subscription instance, prorated per calendar day. Model:
SubscriptionCostLine — see Cost lines. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
billable_subscription_id |
unsignedBigInteger | no | FK larsasub_billable_subscriptions |
key |
string(64) | no | Your identifier for the line, e.g. module:agenda |
description |
string | no | |
amount |
decimal(10,2) | no | Monthly amount |
starts_on |
date | no | Inclusive |
ends_on |
date | yes | Inclusive; null = open-ended |
billed_through |
date | yes | Last day covered by a generated charge |
valued_from |
date | yes | First day of the stretch billed_through closes that was actually valued. Read it through SubscriptionCostLine::valuedFrom(), never as a raw attribute |
deleted_at |
timestamp | yes | Soft delete |
valued_from and starts_on are not each other's mirror: an opening charge
clamps to its own period start and writes the days before it off. Null means
"nothing valued yet" on a new line and "predates the column" on an old one, and
the accessor answers both with starts_on. Lines marked before the column
existed are repaired once with larsasub:backfill-valued-from — see the
upgrade guide.
Indexes: (billable_subscription_id, key).
Payments
Payment methods, the two payment tables and what hangs off them: order lines, attached entities, reminder series and payouts.
larsasub_payment_methods
The available payment methods. Model: PaymentMethod — see
Enabling methods.
| Column | Type | Null | Notes |
|---|---|---|---|
name |
string(64) | no | |
description |
string(510) | no | |
provider |
string(32) | no | e.g. mollie |
provider_id |
string(12) | no | The provider's method id |
active |
boolean | no | Default false |
tenant_usable |
boolean | no | Default false. Whether a Connect tenant may offer it |
recurring |
boolean | no | Default false |
single |
boolean | no | Default false |
first_payment_method_id |
unsignedBigInteger | yes | The method that opens a mandate for this one. No foreign key |
internal_note |
string(510) | yes |
Indexes: unique (provider, provider_id).
larsasub_tenant_payment_methods
Per-tenant enablement of those methods in Connect mode. Model:
TenantPaymentMethod — see
Tenant availability.
| Column | Type | Null | Notes |
|---|---|---|---|
tenant_id |
unsignedBigInteger | no | |
tenant_type |
string(255) | no | |
payment_method_id |
unsignedBigInteger | no | FK larsasub_payment_methods, cascade on delete |
active |
boolean | no | Default true |
Indexes: unique (tenant_id, tenant_type, payment_method_id).
larsasub_recurring_charges
One payment in a subscription's billing history — and also every charge
without a subscription: standalone charges, the €0.01 mandate setup and
chargeback follow-ups. Model: RecurringCharge — see
Recurring charges. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
reference |
string(16) | yes | Unique payment reference |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables |
billable_subscription_id |
unsignedBigInteger | yes | FK larsasub_billable_subscriptions, set null on delete; null on a standalone charge |
payment_method_id |
unsignedBigInteger | no | FK larsasub_payment_methods. A manual payment overwrites it with the method the customer picks |
amount |
decimal(10,2) | no | The amount collected, after credit |
original_amount |
decimal(10,2) | yes | Before credit |
credit_applied |
decimal(10,2) | yes | |
platform_fee_amount |
decimal(10,2) | yes | The Connect application fee charged |
platform_fee_type |
string(16) | yes | The fee rule it came from |
platform_fee_percentage |
decimal(5,2) | yes | |
currency |
string(6) | no | Default currency.default |
notify |
boolean | no | Default false. Advance-notice bookkeeping |
description |
string | yes | |
amount_breakdown |
longText | yes | Composition snapshot (base price + cost lines), taken at planning |
vat_rate |
decimal(5,2) | yes | The effective rate after the country rules; 0.00 = zero-rated, null = no snapshot |
vat_treatment |
string(16) | no | Default excluded. Enums\VatTreatment value — see the table |
vat_reverse_charge |
boolean | no | Default false. Frozen, because the invoice prints it |
vat_customer |
longText | yes | Encrypted. The customer fields the VAT decision rested on, frozen when the money moved |
planned_on |
date | no | The day the charge is due |
notified_at |
dateTime | yes | |
charged_at |
dateTime | yes | When it was claimed for collection, written before the provider call |
paid_on |
dateTime | yes | |
status_update_at |
dateTime | yes | |
provider_payment_id |
string(32) | yes | |
provider_mandate_type, provider_mandate_id |
morph | yes | |
provider_payment_status |
string(32) | yes | |
provider_payment_status_raw |
string(32) | yes | Reserved for the provider's own status word; nothing writes it yet |
settlement_id |
string(32) | yes | The provider payout this payment was paid out in — see below |
provider_url |
string | yes | |
provider_payment_details |
longText | yes | Encrypted |
provider_chargeback_details |
longText | yes | Encrypted |
provider_refund_details |
longText | yes | Encrypted |
needs_attention |
boolean | no | Default false |
attention_message |
string(128) | yes | Opens with a marker constant — see Public API |
needs_attention_since |
dateTime | yes | |
payable |
boolean | no | Default false. Claim and webhook bookkeeping, not "the customer may pay this" |
mandate_setup |
boolean | no | Default false. The €0.01 verification charge; never invoiced |
superseded_by_charge_id |
unsignedBigInteger | yes | FK to this table, set null on delete |
follows_charge_id |
unsignedBigInteger | yes | On a chargeback follow-up: the charge it collects again. FK to this table, set null on delete |
origin_chargeback_id |
string(32) | yes | Unique. On a follow-up: the chargeback it answers |
origin_reversed_at |
dateTime | yes | On a follow-up: when the bank reversed that chargeback |
reversal_checked_at |
dateTime | yes | On a charged-back original: when the package last asked the provider whether the chargeback was reversed |
deleted_at |
timestamp | yes | Soft delete |
Indexes:
- Unique
reference; uniqueorigin_chargeback_id(one follow-up per chargeback). billable_id,superseded_by_charge_id,follows_charge_id,settlement_id, theprovider_mandatemorph pair.(provider_payment_id, charged_at),(billable_subscription_id, planned_on)and(charged_at, planned_on)— the webhook lookup, the planner and the executor.
larsasub_single_payments
One-time payments. Model: SinglePayment — see
Single payments. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
reference |
string(16) | yes | Unique payment reference |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables |
payment_method_id |
unsignedBigInteger | no | FK larsasub_payment_methods. Overwritten with the method the customer picks when they start the payment |
amount |
decimal(10,2) | no | The amount collected, as you supplied it |
currency |
string(6) | no | Default currency.default |
description |
string | yes | |
vat_rate |
decimal(5,2) | yes | The payment-level rate; 0.00 = zero-rated, null = resolve live. With order lines carrying their own rate, the lines hold the authoritative split |
vat_treatment |
string(16) | no | Default excluded. Enums\VatTreatment value |
vat_reverse_charge |
boolean | no | Default false |
vat_customer |
longText | yes | Encrypted, as on charges |
platform_fee_amount |
decimal(10,2) | yes | Connect application fee |
platform_fee_type |
string(16) | yes | |
platform_fee_percentage |
decimal(5,2) | yes | |
charged_at |
dateTime | yes | |
paid_on |
dateTime | yes | |
status_update_at |
dateTime | yes | |
provider_payment_id |
string(32) | yes | |
provider_payment_status |
string(32) | yes | |
provider_payment_status_raw |
string(32) | yes | Reserved; nothing writes it yet |
settlement_id |
string(32) | yes | The provider payout — see below |
provider_url |
string | yes | |
provider_payment_details |
longText | yes | Encrypted |
provider_chargeback_details |
longText | yes | Encrypted |
provider_refund_details |
longText | yes | Encrypted |
needs_attention |
boolean | no | Default false |
attention_message |
string(128) | yes | |
needs_attention_since |
dateTime | yes | |
payable |
boolean | no | Default true |
deleted_at |
timestamp | yes | Soft delete |
Indexes: unique reference; billable_id; provider_payment_id;
settlement_id.
settlement_id on both payment tables is written by
larsasub:sync-settlements and by nothing else. It is the provider's
settlement id, not a foreign key: a payment is attached before, or without, its
larsasub_settlements row. See Settlements.
larsasub_single_payment_entities
Pivot linking a single payment to your IsPayable business objects. Model:
SinglePaymentEntity — see
Attaching entities.
| Column | Type | Null | Notes |
|---|---|---|---|
single_payment_id |
unsignedBigInteger | no | FK larsasub_single_payments, cascade on delete |
entity_type, entity_id |
morph | no | Your IsPayable model |
processed_at |
timestamp | yes | Set once onPaymentPaid() ran for this entity, so a redelivered webhook does not run it twice |
Indexes: single_payment_id; the entity morph pair.
larsasub_single_payment_lines
Optional order lines on a single payment. Model: SinglePaymentLine — see
Order lines.
| Column | Type | Null | Notes |
|---|---|---|---|
single_payment_id |
unsignedBigInteger | no | FK larsasub_single_payments, cascade on delete |
description |
string | no | |
quantity |
decimal(8,2) | no | Default 1 |
unit_amount |
decimal(10,2) | no | |
amount |
decimal(10,2) | no | Line total, in the payment amount's terms |
vat_rate |
decimal(5,2) | yes | Per-line rate; null = the payment's own vat_rate, else invoicing.vat_rate |
sort_order |
unsignedSmallInteger | no | Default 0 |
Indexes: single_payment_id.
larsasub_dunning_cases
One reminder series per unpaid charge. Written only through DunningService;
no soft deletes — the model log is the trail. Model: DunningCase — see
Reminder series, which has a table for every
enum value below.
| Column | Type | Null | Notes |
|---|---|---|---|
recurring_charge_id |
unsignedBigInteger | no | Unique. FK larsasub_recurring_charges |
billable_id |
unsignedBigInteger | no | FK larsasub_billables, copied from the charge |
origin |
string(32) | no | Enums\DunningOrigin value |
schedule |
json | no | The steps as configured when the case opened |
started_at |
dateTime | no | The moment the steps count from |
active_since |
dateTime | no | Opening or the last resume; no step runs within a day of it |
status |
string(32) | no | Enums\DunningStatus value |
pause_reason |
string(32) | yes | Enums\DunningPauseReason value, while paused |
paused_since |
dateTime | yes | |
closed_reason |
string(32) | yes | Enums\DunningClosedReason value, when closed |
resolved_at |
dateTime | yes | |
assessed_at |
dateTime | yes | The last run that looked at the case |
started_dispatched_at |
dateTime | yes | Outbox marker for the started event |
resolution_dispatched_at |
dateTime | yes | Outbox marker for the resolved event |
Indexes: unique recurring_charge_id; billable_id; status.
larsasub_dunning_steps
One row per step of a case's schedule that a run took up. Model:
DunningStep.
| Column | Type | Null | Notes |
|---|---|---|---|
dunning_case_id |
unsignedBigInteger | no | FK larsasub_dunning_cases |
step_index |
unsignedSmallInteger | no | Position in the case's own schedule, from 0 |
action |
string(32) | no | Enums\DunningAction value |
result |
string(32) | no | Enums\DunningStepResult value |
executed_at |
dateTime | no | |
dispatched_at |
dateTime | yes | Outbox marker: set once the step's events went out |
Indexes: unique (dunning_case_id, step_index) — two overlapping runs cannot
both take the same step.
larsasub_settlements
The provider's payouts, one row per settlement, written by
larsasub:sync-settlements. Model: Settlement — see
Settlements.
| Column | Type | Null | Notes |
|---|---|---|---|
tenant_type |
string(255) | yes | The connected account; null = the platform's own account. Never overwritten |
tenant_id |
unsignedBigInteger | yes | |
settlement_id |
string(32) | no | Unique. The provider's id (stl_…) |
reference |
string(64) | yes | As printed on the bank statement; absent while the settlement is open |
status |
string(32) | no | The provider's own word, stored unchanged |
amount |
decimal(10,2) | no | The paid-out amount |
currency |
string(6) | no | |
settled_at |
dateTime | yes | |
provider_created_at |
dateTime | no | |
invoice_id |
string(32) | yes | The provider's invoice id |
periods |
json | yes | The provider's revenue and costs breakdown |
synced_at |
dateTime | yes | When every payment of the settlement was last attached; null until the first complete pass |
Indexes: unique settlement_id; (tenant_type, tenant_id); reference;
status.
Credit
The credit wallet and its ledger. Mutate them only through CreditService —
see Credit.
larsasub_billable_credits
One credit account per billable. Model: BillableCredit.
| Column | Type | Null | Notes |
|---|---|---|---|
billable_id |
unsignedBigInteger | yes | Unique. FK larsasub_billables |
balance |
decimal(10,2) | no | Default 0.00 |
currency |
string(6) | no | Default currency.default. The one currency the balance is held in; credit is applied only to a charge in it |
Indexes: unique billable_id.
larsasub_credit_transactions
The ledger: one row per credit mutation. Model: CreditTransaction — see
Transaction types.
| Column | Type | Null | Notes |
|---|---|---|---|
billable_credit_id |
unsignedBigInteger | no | FK larsasub_billable_credits, cascade on delete |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables, set null on delete. Denormalized for direct querying |
amount |
decimal(10,2) | no | Positive for additions, negative for deductions |
remaining_amount |
decimal(10,2) | yes | On additions only: how much of this credit is still unspent |
balance_before |
decimal(10,2) | no | |
balance_after |
decimal(10,2) | no | |
type |
string(32) | no | One of the CreditTransaction::TYPE_* constants |
description |
string(255) | yes | |
source_type, source_id |
morph | yes | What caused the transaction |
recurring_charge_id |
unsignedBigInteger | yes | FK larsasub_recurring_charges, set null on delete |
expires_at |
timestamp | yes | |
expired |
boolean | no | Default false |
expiration_notified_at |
timestamp | yes | |
performed_by_user_id |
unsignedBigInteger | yes | Your auth user. No foreign key: the user table may live in another database |
meta |
json | yes |
Indexes: billable_credit_id, billable_id, type, recurring_charge_id,
expires_at, expired, performed_by_user_id, the source morph pair.
Invoicing
Invoice and credit-note snapshots and their numbering. Invoicing is off by default — see Invoicing and VAT.
larsasub_invoices
Finalized invoice snapshots. Model: Invoice — see
The invoice model.
| Column | Type | Null | Notes |
|---|---|---|---|
invoice_number |
string(32) | no | Unique |
status |
enum(finalized, voided) |
no | Default finalized |
billable_id |
unsignedBigInteger | no | FK larsasub_billables |
invoiceable_type, invoiceable_id |
morph | yes | The payment: a RecurringCharge or a SinglePayment |
tenant_type, tenant_id |
morph | yes | The seller in Connect mode |
customer_name |
string | no | Customer snapshot |
customer_email |
string | yes | |
customer_company_name |
string | yes | |
customer_vat_number |
string(32) | yes | |
customer_coc_number |
string(32) | yes | |
customer_address |
text | yes | |
customer_country |
string(2) | yes | |
customer_vat_check_reference |
string(255) | yes | The VIES consultation number behind a reverse charge. Never printed on the PDF |
seller_name |
string | no | Seller snapshot |
seller_vat_number |
string(32) | yes | |
seller_coc_number |
string(32) | yes | |
seller_address |
text | yes | |
seller_country |
string(2) | yes | |
subtotal |
decimal(10,2) | no | Net of VAT |
vat_amount |
decimal(10,2) | no | Default 0. Always total − subtotal |
total |
decimal(10,2) | no | The charge's full value including VAT, before credit (original_amount). total − credit_applied is what the customer was debited |
currency |
string(3) | no | The currency the payment was collected in (before 0.38.1: currency.default for a charge and for a single payment without order lines) |
vat_rate |
decimal(5,2) | yes | Default 0. Null = mixed per-line rates |
reverse_charge |
boolean | no | Default false |
credit_applied |
decimal(10,2) | no | Default 0 |
description |
string | no | |
invoice_date |
date | no | |
payment_method_label |
string | yes | |
external_download_url |
string(512) | yes | Set when your own invoicing system produces the document |
Indexes: unique invoice_number; unique (invoiceable_type, invoiceable_id) —
at most one invoice per payment; billable_id; the tenant morph pair.
larsasub_invoice_lines
Immutable invoice lines, net of VAT, summing exactly to the invoice subtotal.
Model: InvoiceLine — see Invoice lines.
| Column | Type | Null | Notes |
|---|---|---|---|
invoice_id |
unsignedBigInteger | no | FK larsasub_invoices, cascade on delete |
key |
string(64) | yes | The cost line key; null for base and single-payment lines |
description |
string | no | |
quantity |
decimal(8,2) | no | Default 1 |
unit_amount |
decimal(10,2) | no | |
amount |
decimal(10,2) | no | Net of VAT |
vat_rate |
decimal(5,2) | yes | Null = the invoice-level rate applies |
vat_amount |
decimal(10,2) | yes | |
period_start |
date | yes | Billed period of a subscription or cost line |
period_end |
date | yes | |
sort_order |
unsignedSmallInteger | no | Default 0 |
Indexes: invoice_id.
larsasub_credit_notes
Credit notes voiding an invoice after a refund or chargeback. Model:
CreditNote — see Credit notes.
| Column | Type | Null | Notes |
|---|---|---|---|
credit_note_number |
string(32) | no | Unique |
invoice_id |
unsignedBigInteger | no | FK larsasub_invoices |
billable_id |
unsignedBigInteger | no | FK larsasub_billables |
tenant_type, tenant_id |
morph | yes | |
total |
decimal(10,2) | no | |
vat_amount |
decimal(10,2) | no | Default 0 |
currency |
string(3) | no | The invoice's currency |
vat_rate |
decimal(5,2) | yes | Null = mixed per-line rates on the voided invoice |
reverse_charge |
boolean | no | Default false |
reason |
string | yes | |
external_download_url |
string(512) | yes | |
issued_at |
date | no |
Indexes: unique credit_note_number; invoice_id; billable_id; the tenant
morph pair.
larsasub_invoice_sequences
Continuous numbering, one sequence per document type and tenant, never reset.
Model: InvoiceSequence — see Numbering.
| Column | Type | Null | Notes |
|---|---|---|---|
tenant_type, tenant_id |
morph | yes | |
tenant_key |
string | no | Default ''. Non-null mirror of the tenant, as on larsasub_billables |
type |
string(16) | no | invoice or credit_note |
last_number |
unsignedInteger | no | Default 0 |
Indexes: unique (tenant_key, type) — the non-null tenant_key is what makes
it bite in simple mode, where a unique over the nullable morph would let two
first invoices mint the same number.
Provider (Mollie)
Customer and mandate references at Mollie, and the Connect tables. See Mollie models.
larsasub_mollie_customers
Customer references at Mollie, one per billable per tenant. Model:
MollieCustomer. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
customer_id |
string(32) | no | Mollie's customer id (cst_…) |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables |
tenant_id |
unsignedBigInteger | yes | |
tenant_type |
string | yes | |
deleted_at |
timestamp | yes | Soft delete |
Indexes: unique (billable_id, tenant_id, tenant_type).
larsasub_mollie_mandates
Direct debit mandates. Model: MollieMandate — see
Mandates. Soft deletes.
| Column | Type | Null | Notes |
|---|---|---|---|
mandate_id |
string(32) | no | Mollie's mandate id (mdt_…) |
status |
string(32) | no | |
consumer_account |
text | no | Encrypted. Not null |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables |
customer_id |
unsignedBigInteger | no | FK larsasub_mollie_customers — a mandate requires a customer |
payment_method_id |
unsignedBigInteger | no | FK larsasub_payment_methods |
details |
longText | yes | Encrypted |
usable |
boolean | no | Default false |
last_validated_at |
timestamp | yes | Resume cursor for larsasub:validate-mandates |
deleted_at |
timestamp | yes | Soft delete |
The tenant of a mandate is the tenant of its customer.
Indexes: unique (customer_id, mandate_id); (billable_id, payment_method_id);
last_validated_at.
larsasub_mollie_connected_accounts
A tenant's OAuth connection to its own Mollie organization. Model:
MollieConnectedAccount — see Mollie Connect.
| Column | Type | Null | Notes |
|---|---|---|---|
reference_code |
string(4) | yes | Unique. The tenant's code in payment references, e.g. LCRA |
tenant_id |
unsignedBigInteger | no | |
tenant_type |
string | no | |
access_token |
text | no | Encrypted |
refresh_token |
text | no | Encrypted |
expires_at |
timestamp | yes | Access token expiry |
mollie_organization_id |
string | yes | |
organization_name |
string | yes | |
mollie_profile_id |
string | yes | The website profile payments are created on. See Choosing the website profile |
mollie_profile_name |
string | yes | |
mollie_profile_count |
unsignedInteger | yes | |
profile_selected_at |
timestamp | yes | Set when the tenant chose the profile themselves |
connected_at |
timestamp | yes | |
disconnected_at |
timestamp | yes | |
disconnect_requested_at |
timestamp | yes | A requested disconnect that is draining. See Disconnecting a tenant |
disconnect_deadline_at |
timestamp | yes | Frozen at the request |
settlements_synced_at |
timestamp | yes | The last successful settlement sync |
settlements_sync_error |
string(64) | yes | Enums\SettlementSyncOutcome value: why the last pass could not run |
Indexes: unique (tenant_id, tenant_type) — one connected account per tenant;
unique reference_code; disconnected_at.
larsasub_connect_flow_states
A Connect authorization in progress. It lives in a table rather than the cache
because the flow crosses origins — see
The OAuth flow. Model: ConnectFlowState.
| Column | Type | Null | Notes |
|---|---|---|---|
state_hash |
char(64) | yes | Unique. SHA-256 of the OAuth state; cleared when consumed |
ticket_hash |
char(64) | yes | Unique. SHA-256 of the completion ticket; cleared when consumed |
tenant_type |
string | no | The tenant being connected |
tenant_id |
unsignedBigInteger | no | |
user_key |
string(191) | no | The user who started the flow; only they can complete it |
origin |
string | no | Where the initiating session lives |
return_url |
text | no | |
code |
text | yes | Encrypted. The authorization code, present only between the callback and completion |
expires_at |
timestamp | no |
The rows are transient: a flow that completes or is declined is deleted on the spot, and expired rows are swept by the next authorization. That is why the table has no prune command.
Indexes: unique state_hash; unique ticket_hash; expires_at.
Supporting
Acceptance evidence, the notification delivery log and the activity trail. None of these tables stores a raw IP address, user agent, email address, subject or body — see Compliance.
larsasub_terms_acceptances
Append-only acceptance evidence. Model: TermsAcceptance — see
The evidence record.
| Column | Type | Null | Notes |
|---|---|---|---|
billable_id |
unsignedBigInteger | no | FK larsasub_billables |
billable_subscription_id |
unsignedBigInteger | yes | FK larsasub_billable_subscriptions; linked after creation succeeds |
terms_identifier |
string(128) | no | Your document version identifier, verbatim. Not a foreign key |
terms_locale |
string(12) | yes | The language rendition shown |
context |
string(32) | no | What was accepted: subscribe (the terms), or withdrawal-consent (the waiver of the withdrawal right) — see Withdrawal consent |
accepted_at |
dateTime | no | |
client_hash |
string(64) | yes | Keyed one-way hash of IP address and user agent — never the raw values |
Indexes: (billable_id, terms_identifier).
larsasub_notification_deliveries
One row per (notification, channel), so an interrupted delivery job does not
re-mail a whole list. Model: NotificationDelivery — see
Delivery records and duplicates.
| Column | Type | Null | Notes |
|---|---|---|---|
notification_id |
uuid | no | Laravel's notification id |
channel |
string(191) | no | |
delivery_key |
uuid | yes | The queued delivery that produced the row |
notification |
string(191) | no | The notification class, never its subject or body |
recipient_type |
string | yes | The recipient as a model reference, never an address |
recipient_id |
unsignedBigInteger | yes | |
recipient_hash |
char(64) | yes | Keyed one-way hash of a model-less recipient's address |
billable_id |
unsignedBigInteger | yes | FK larsasub_billables, cascade on delete |
tenant_type |
string | yes | |
tenant_id |
unsignedBigInteger | yes | |
status |
string(16) | no | Default sending |
attempts |
unsignedTinyInteger | no | Default 0 |
claimed_until |
timestamp | yes | Lease of the current attempt |
sent_at |
timestamp | yes | |
failure |
text | yes | The exception class of a failed attempt |
Indexes: unique (notification_id, channel) — the deduplication key;
(delivery_key, status); (billable_id, created_at);
(status, created_at); created_at.
larsasub_model_logs
The per-model activity trail, written through
peters-development/laravel-model-logger. Model: ModelLog — see
HasModelLogs. This table has created_at only.
| Column | Type | Null | Notes |
|---|---|---|---|
user_id |
unsignedBigInteger | yes | The acting user. Indexed |
message |
text | no | The line, rendered in the log language |
loggable_type, loggable_id |
morph | no | The model the line is about |
meta |
json | yes | Includes the translation key and parameters the line was written with |
created_at |
timestamp | yes |
Indexes: user_id; the loggable morph pair.
Three daily prune commands keep the bounded tables bounded:
| Command | Table | Retention key | Default |
|---|---|---|---|
larsasub:prune-model-logs |
larsasub_model_logs |
retention.model_logs_days |
730 days |
larsasub:prune-notification-deliveries |
larsasub_notification_deliveries |
retention.notification_deliveries_days |
365 days |
larsasub:prune-dunning-cases |
larsasub_dunning_cases and their steps |
retention.dunning_days |
kept (null) |
For the first two, null or 0 disables pruning. Finished reminder series are
kept unless you set retention.dunning_days; a series whose charge could still
get one is never pruned.
Keeping the schema in step
Since 0.37.1, existing migrations never change: every schema change ships
as a new migration, and php artisan migrate applies it. Up to 0.37.1 the
migrations were edited in place, so a database created from an earlier release
can have nothing pending while it misses columns: php artisan migrate
reports success, and the next scheduled run dies on Unknown column.
php artisan larsasub:doctor
The doctor checks the columns the models expect, not just the tables. On a
development database the fix is php artisan migrate:fresh, or a manual
ALTER — see the upgrade guide for the per-release notes.