Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
(STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E ✅, Checkliste,
STATUS-ABGLEICH, PROGRESS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
38fab64e10
commit
c8dc99c3c8
24 changed files with 775 additions and 100 deletions
|
|
@ -11,7 +11,9 @@
|
|||
Wird in Detailansicht, Bearbeiten und Erstellen eingebunden. Der
|
||||
`action`-Prop bestimmt die Livewire-Methode der Eltern-Komponente, die beim
|
||||
Bestätigen ausgeführt wird (z. B. `submitForReview`, `saveAndSubmit`,
|
||||
`save('review')`). Quota-Block wird nur angezeigt, wenn Werte übergeben sind.
|
||||
`save('review')`). Quota-Block wird nur angezeigt, wenn Werte übergeben
|
||||
sind — null bedeutet unbegrenztes Kontingent (Bestandsschutz bzw.
|
||||
Launch-Schalter aus) und blendet den Block aus.
|
||||
|
||||
Submit-Gate (Decision-Update §5.1): Ohne aktive Buchung zeigt das Modal
|
||||
statt des Prüf-Flows einen Buchungs-Hinweis — der Button konvertiert,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
|||
public function with(): array
|
||||
{
|
||||
return [
|
||||
// Rückkehr aus dem Stripe-Checkout (?checkout=erfolg|abbruch)
|
||||
// bzw. Hinweis aus den Checkout-Guards (Session-Flash).
|
||||
'checkoutResult' => request()->query('checkout'),
|
||||
'checkoutNotice' => session('checkout-notice'),
|
||||
'creditSummary' => [
|
||||
'total' => 17,
|
||||
'bonus' => 12,
|
||||
|
|
@ -99,6 +103,34 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
|
|||
}; ?>
|
||||
|
||||
<div class="space-y-8">
|
||||
{{-- ============== CHECKOUT-RÜCKMELDUNG ============== --}}
|
||||
@if ($checkoutResult === 'erfolg')
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
||||
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
|
||||
<flux:icon.check-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Vielen Dank für Ihre Buchung!') }}</span>
|
||||
{{ __('Die Zahlung wird von Stripe bestätigt — die Buchung erscheint hier in wenigen Augenblicken. Die Rechnung finden Sie anschließend unter Rechnungen.') }}
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($checkoutResult === 'abbruch')
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
||||
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
|
||||
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
|
||||
<div class="flex-1">
|
||||
{{ __('Der Bezahlvorgang wurde abgebrochen. Es wurde nichts gebucht — Sie können den Checkout jederzeit erneut starten.') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($checkoutNotice)
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
|
||||
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
|
||||
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
|
||||
<div class="flex-1">{{ $checkoutNotice }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
<div class="min-w-0">
|
||||
|
|
|
|||
|
|
@ -546,7 +546,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
|
|||
: Contact::query()->whereRaw('0 = 1')->get(),
|
||||
'selectedPortalLabel' => $selectedCompany?->portal?->label() ?? __('Wird aus der Firma übernommen'),
|
||||
'tagSuggestions' => $this->tagSuggestionsFor($selectedCompany),
|
||||
'quotaTotal' => (int) $user->press_release_quota,
|
||||
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
|
|||
'tagSuggestions' => $this->tagSuggestionsFor($selectedCompany),
|
||||
'coverUrl' => $cover->coverUrl($pressRelease, 'cover'),
|
||||
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pressRelease),
|
||||
'quotaTotal' => (int) $user->press_release_quota,
|
||||
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
'categoryName' => $categoryName,
|
||||
'coverUrl' => $cover->coverUrl($pr, 'cover'),
|
||||
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
|
||||
'quotaTotal' => (int) $user->press_release_quota,
|
||||
'quotaTotal' => $user->pressReleaseQuotaTotal(),
|
||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||
'canEdit' => auth()->user()->can('update', $pr)
|
||||
&& in_array($pr->status->value, ['draft', 'rejected']),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue