coupon code system eingeführt. coupons werden vom super admin gemanaged. coupons werden mit paddle synchronisiert und dort validiert. plus: einige mobil-optimierungen im tenant admin pwa.

This commit is contained in:
Codex Agent
2025-11-09 20:26:50 +01:00
parent f3c44be76d
commit 082b78cd43
80 changed files with 4855 additions and 435 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Services\Checkout;
use App\Models\CheckoutSession;
use App\Models\Coupon;
use App\Models\Package;
use App\Models\Tenant;
use App\Models\User;
@@ -64,6 +65,7 @@ class CheckoutSessionService
$session->package_snapshot = $this->packageSnapshot($package);
$session->amount_subtotal = Arr::get($session->package_snapshot, 'price', 0);
$session->amount_total = Arr::get($session->package_snapshot, 'price', 0);
$session->amount_discount = 0;
$session->provider = CheckoutSession::PROVIDER_NONE;
$session->status = CheckoutSession::STATUS_DRAFT;
$session->stripe_payment_intent_id = null;
@@ -73,6 +75,10 @@ class CheckoutSessionService
$session->paddle_transaction_id = null;
$session->provider_metadata = [];
$session->failure_reason = null;
$session->coupon()->dissociate();
$session->coupon_code = null;
$session->coupon_snapshot = [];
$session->discount_breakdown = [];
$session->expires_at = now()->addMinutes($this->sessionTtlMinutes);
$this->appendStatus($session, CheckoutSession::STATUS_DRAFT, 'package_switched');
$session->save();
@@ -81,6 +87,31 @@ class CheckoutSessionService
});
}
public function applyCoupon(CheckoutSession $session, Coupon $coupon, array $pricing): CheckoutSession
{
$snapshot = [
'coupon' => [
'id' => $coupon->id,
'code' => $coupon->code,
'type' => $coupon->type?->value,
],
'pricing' => $pricing,
];
$session->coupon()->associate($coupon);
$session->coupon_code = $coupon->code;
$session->coupon_snapshot = $snapshot;
$session->amount_subtotal = $pricing['subtotal'] ?? $session->amount_subtotal;
$session->amount_discount = $pricing['discount'] ?? 0;
$session->amount_total = $pricing['total'] ?? $session->amount_total;
$session->discount_breakdown = is_array($pricing['breakdown'] ?? null)
? $pricing['breakdown']
: [];
$session->save();
return $session->refresh();
}
public function selectProvider(CheckoutSession $session, string $provider): CheckoutSession
{
$provider = strtolower($provider);

View File

@@ -6,6 +6,7 @@ use App\Models\CheckoutSession;
use App\Models\Package;
use App\Models\Tenant;
use App\Models\TenantPackage;
use App\Services\Coupons\CouponRedemptionService;
use App\Services\Paddle\PaddleSubscriptionService;
use Carbon\Carbon;
use Illuminate\Support\Arr;
@@ -19,6 +20,7 @@ class CheckoutWebhookService
private readonly CheckoutSessionService $sessions,
private readonly CheckoutAssignmentService $assignment,
private readonly PaddleSubscriptionService $paddleSubscriptions,
private readonly CouponRedemptionService $couponRedemptions,
) {}
public function handleStripeEvent(array $event): bool
@@ -216,6 +218,7 @@ class CheckoutWebhookService
]);
$this->sessions->markCompleted($session, now());
$this->couponRedemptions->recordSuccess($session, $data);
}
return true;
@@ -224,6 +227,7 @@ class CheckoutWebhookService
case 'transaction.cancelled':
$reason = $status ?: ($eventType === 'transaction.failed' ? 'paddle_failed' : 'paddle_cancelled');
$this->sessions->markFailed($session, $reason);
$this->couponRedemptions->recordFailure($session, $reason);
return true;