Phase 9D: Tarif-Datenmodell, Cashier und hybride Rechnungskreise STR-/MAN-

Tarif-Datenmodell (Decision-Update):
- plans: Starter/Business/Pro/Agency mit Monats-/Jahrespreis (Jahres =
  10 x Monat), PM-Kontingent, Tageslimit, Stripe-IDs; idempotenter Seeder
- single_purchases: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit
  Status-Lifecycle und Stripe-Checkout-Referenzen
- laravel/cashier ^16.5 installiert (freigegeben); User ist Billable,
  Cashier-Migrationen published + ausgefuehrt; lokale invoices()-Relation
  ueberschreibt bewusst die Cashier-Methode

Hybride Rechnungskreise (Entscheidung 12.06.2026):
- invoice_number_sequences + InvoiceNumberGenerator: atomare fortlaufende
  Nummern pro Kreis (STR- fuer den neuen Stripe-Shop, MAN- fuer den
  manuellen Legacy-Kreis); Alt-Archiv legacy_invoices bleibt unveraendert
- ManualInvoiceService + billing:generate-manual-invoices (Scheduler
  taeglich 04:30): prueft aktive/grandfathered user_payment_options ohne
  Stripe-Subscription auf erreichtes Periodenende, friert die
  Rechnungsadresse als Snapshot ein, stellt die MAN-Rechnung aus
  (Zahlungsziel billing.manual_due_days) und schaltet die Periode weiter;
  Konditions-Overrides via legacy_conditions, sonst Netto-Preis +
  billing.vat_rate; nicht abrechenbare Faelle werden geloggt und
  beim naechsten Lauf erneut geprueft

Submit-Gate:
- User::hasActiveBooking() prueft jetzt echt (hinter
  billing.enforce_booking): Cashier-Abo, bezahlter Einzel-/Extra-PM-Kauf
  oder laufende Legacy-Vereinbarung (MAN-Kreis)

Suite: 468 passed, 4 skipped (17 neue Billing-Tests). Pint clean.
Offen fuer 9E: Stripe-Checkout/Webhooks, STR-Spiegelung, Slot-Logik auf
Plan-Kontingent, Migration der aktiven Legacy-Zahlungen in
user_payment_options.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 10:15:46 +00:00
parent 4419d9ff43
commit d548f4b235
28 changed files with 1545 additions and 25 deletions

328
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cbc29fc1cf64ca319c7c0ef7e0c1088c",
"content-hash": "7ad3d072c1669ef5d37e58ba10187b58",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -1369,6 +1369,95 @@
],
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "laravel/cashier",
"version": "v16.5.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/cashier-stripe.git",
"reference": "b6bcd6b4d79acead34d00a5a528c904d67c5e08a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/b6bcd6b4d79acead34d00a5a528c904d67c5e08a",
"reference": "b6bcd6b4d79acead34d00a5a528c904d67c5e08a",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^10.0|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"illuminate/database": "^10.0|^11.0|^12.0|^13.0",
"illuminate/http": "^10.0|^11.0|^12.0|^13.0",
"illuminate/log": "^10.0|^11.0|^12.0|^13.0",
"illuminate/notifications": "^10.0|^11.0|^12.0|^13.0",
"illuminate/pagination": "^10.0|^11.0|^12.0|^13.0",
"illuminate/routing": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"illuminate/view": "^10.0|^11.0|^12.0|^13.0",
"moneyphp/money": "^4.0",
"nesbot/carbon": "^2.0|^3.0",
"php": "^8.1",
"stripe/stripe-php": "^17.3.0",
"symfony/console": "^6.0|^7.0|^8.0",
"symfony/http-kernel": "^6.0|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.22.1",
"symfony/polyfill-php84": "^1.32"
},
"require-dev": {
"dompdf/dompdf": "^2.0|^3.0",
"orchestra/testbench": "^8.36|^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10",
"spatie/laravel-ray": "^1.40"
},
"suggest": {
"dompdf/dompdf": "Required when generating and downloading invoice PDF's using Dompdf (^2.0|^3.0).",
"ext-intl": "Allows for more locales besides the default \"en\" when formatting money values.",
"spatie/laravel-pdf": "Required when generating and downloading invoice PDF's using Cashier's LaravelPdfInvoiceRenderer."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Cashier\\CashierServiceProvider"
]
},
"branch-alias": {
"dev-master": "16.x-dev"
}
},
"autoload": {
"psr-4": {
"Laravel\\Cashier\\": "src/",
"Laravel\\Cashier\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Dries Vints",
"email": "dries@laravel.com"
}
],
"description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.",
"keywords": [
"billing",
"laravel",
"stripe"
],
"support": {
"issues": "https://github.com/laravel/cashier/issues",
"source": "https://github.com/laravel/cashier"
},
"time": "2026-05-05T21:18:35+00:00"
},
{
"name": "laravel/fortify",
"version": "v1.36.2",
@ -2826,6 +2915,96 @@
},
"time": "2026-04-15T16:41:08+00:00"
},
{
"name": "moneyphp/money",
"version": "v4.9.0",
"source": {
"type": "git",
"url": "https://github.com/moneyphp/money.git",
"reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/moneyphp/money/zipball/d49ee625c6ba79b9d7a228ce153b02fc1032152b",
"reference": "d49ee625c6ba79b9d7a228ce153b02fc1032152b",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"ext-filter": "*",
"ext-json": "*",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"cache/taggable-cache": "^1.1.0",
"doctrine/coding-standard": "^12.0",
"doctrine/instantiator": "^1.5.0 || ^2.0",
"ext-gmp": "*",
"ext-intl": "*",
"florianv/exchanger": "^2.8.1",
"florianv/swap": "^4.3.0",
"moneyphp/crypto-currencies": "^1.1.0",
"moneyphp/iso-currencies": "^3.4",
"php-http/message": "^1.16.0",
"php-http/mock-client": "^1.6.0",
"phpbench/phpbench": "^1.2.5",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1.9",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5.9",
"psr/cache": "^1.0.1 || ^2.0 || ^3.0",
"ticketswap/phpstan-error-formatter": "^1.1"
},
"suggest": {
"ext-gmp": "Calculate without integer limits",
"ext-intl": "Format Money objects with intl",
"florianv/exchanger": "Exchange rates library for PHP",
"florianv/swap": "Exchange rates library for PHP",
"psr/cache-implementation": "Used for Currency caching"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Money\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mathias Verraes",
"email": "mathias@verraes.net",
"homepage": "http://verraes.net"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
},
{
"name": "Frederik Bosch",
"email": "f.bosch@genkgo.nl"
}
],
"description": "PHP implementation of Fowler's Money pattern",
"homepage": "http://moneyphp.org",
"keywords": [
"Value Object",
"money",
"vo"
],
"support": {
"issues": "https://github.com/moneyphp/money/issues",
"source": "https://github.com/moneyphp/money/tree/v4.9.0"
},
"time": "2026-05-04T20:23:15+00:00"
},
{
"name": "monolog/monolog",
"version": "3.10.0",
@ -4306,6 +4485,65 @@
],
"time": "2026-03-17T22:46:46+00:00"
},
{
"name": "stripe/stripe-php",
"version": "v17.6.0",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=5.6.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.72.0",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^5.7 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Stripe\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stripe and contributors",
"homepage": "https://github.com/stripe/stripe-php/contributors"
}
],
"description": "Stripe PHP Library",
"homepage": "https://stripe.com/",
"keywords": [
"api",
"payment processing",
"stripe"
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v17.6.0"
},
"time": "2025-08-27T19:32:42+00:00"
},
{
"name": "symfony/clock",
"version": "v8.0.8",
@ -5467,6 +5705,94 @@
],
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-icu",
"version": "v1.38.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
"reference": "445c90e341fccda10311019cf82ff73bb7343945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/445c90e341fccda10311019cf82ff73bb7343945",
"reference": "445c90e341fccda10311019cf82ff73bb7343945",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance and support of other locales than \"en\""
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Icu\\": ""
},
"classmap": [
"Resources/stubs"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's ICU-related data and classes",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"icu",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.38.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-05-25T11:52:53+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.36.0",