Änderungen (relevant):

- Add‑on Checkout auf Transactions + Transaction‑ID speichern: app/Services/Addons/EventAddonCheckoutService.php
  - Paket/Marketing Checkout auf Transactions: app/Services/Paddle/PaddleCheckoutService.php
  - Gift‑Voucher Checkout: Customer anlegen/finden + Transactions: app/Services/GiftVouchers/
    GiftVoucherCheckoutService.php
  - Tests aktualisiert: tests/Feature/Tenant/EventAddonCheckoutTest.php, tests/Unit/PaddleCheckoutServiceTest.php,
tests/Unit/GiftVoucherCheckoutServiceTest.php
This commit is contained in:
Codex Agent
2025-12-29 18:04:28 +01:00
parent 795e37ee12
commit 5f521d055f
26 changed files with 783 additions and 102 deletions

View File

@@ -5,8 +5,8 @@ namespace App\Services\Addons;
use App\Models\Event;
use App\Models\EventPackageAddon;
use App\Models\Tenant;
use App\Services\Paddle\PaddleCustomerService;
use App\Services\Paddle\PaddleClient;
use App\Services\Paddle\PaddleCustomerService;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
@@ -86,15 +86,20 @@ class EventAddonCheckoutService
'quantity' => $quantity,
],
],
'metadata' => $metadata,
'success_url' => $payload['success_url'] ?? null,
'cancel_url' => $payload['cancel_url'] ?? null,
'custom_data' => $metadata,
], static fn ($value) => $value !== null && $value !== '');
$response = $this->paddle->post('/checkout/links', $requestPayload);
$response = $this->paddle->post('/transactions', $requestPayload);
$checkoutUrl = Arr::get($response, 'data.url') ?? Arr::get($response, 'url');
$checkoutId = Arr::get($response, 'data.id') ?? Arr::get($response, 'id');
$checkoutUrl = Arr::get($response, 'data.checkout.url')
?? Arr::get($response, 'checkout.url')
?? Arr::get($response, 'data.url')
?? Arr::get($response, 'url');
$checkoutId = Arr::get($response, 'data.checkout_id')
?? Arr::get($response, 'data.checkout.id')
?? Arr::get($response, 'checkout_id')
?? Arr::get($response, 'checkout.id');
$transactionId = Arr::get($response, 'data.id') ?? Arr::get($response, 'id');
if (! $checkoutUrl) {
Log::warning('Paddle addon checkout response missing url', ['response' => $response]);
@@ -108,7 +113,7 @@ class EventAddonCheckoutService
'quantity' => $quantity,
'price_id' => $priceId,
'checkout_id' => $checkoutId,
'transaction_id' => null,
'transaction_id' => $transactionId,
'status' => 'pending',
'metadata' => array_merge($metadata, [
'increments' => $increments,
@@ -126,8 +131,10 @@ class EventAddonCheckoutService
return [
'checkout_url' => $checkoutUrl,
'expires_at' => Arr::get($response, 'data.expires_at') ?? Arr::get($response, 'expires_at'),
'id' => $checkoutId,
'expires_at' => Arr::get($response, 'data.checkout.expires_at')
?? Arr::get($response, 'data.expires_at')
?? Arr::get($response, 'expires_at'),
'id' => $transactionId ?? $checkoutId,
];
}

View File

@@ -2,6 +2,7 @@
namespace App\Services\GiftVouchers;
use App\Services\Paddle\Exceptions\PaddleException;
use App\Services\Paddle\PaddleClient;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
@@ -49,6 +50,8 @@ class GiftVoucherCheckoutService
]);
}
$customerId = $this->ensureCustomerId($data['purchaser_email']);
$payload = [
'items' => [
[
@@ -56,7 +59,7 @@ class GiftVoucherCheckoutService
'quantity' => 1,
],
],
'customer_email' => $data['purchaser_email'],
'customer_id' => $customerId,
'custom_data' => array_filter([
'type' => 'gift_voucher',
'tier_key' => $tier['key'],
@@ -66,15 +69,18 @@ class GiftVoucherCheckoutService
'message' => $data['message'] ?? null,
'app_locale' => App::getLocale(),
]),
'success_url' => $data['success_url'] ?? route('marketing.success', ['locale' => App::getLocale(), 'type' => 'gift']),
'cancel_url' => $data['return_url'] ?? route('packages', ['locale' => App::getLocale()]),
];
$response = $this->client->post('/checkout/links', $payload);
$response = $this->client->post('/transactions', $payload);
return [
'checkout_url' => Arr::get($response, 'data.url') ?? Arr::get($response, 'url'),
'expires_at' => Arr::get($response, 'data.expires_at') ?? Arr::get($response, 'expires_at'),
'checkout_url' => Arr::get($response, 'data.checkout.url')
?? Arr::get($response, 'checkout.url')
?? Arr::get($response, 'data.url')
?? Arr::get($response, 'url'),
'expires_at' => Arr::get($response, 'data.checkout.expires_at')
?? Arr::get($response, 'data.expires_at')
?? Arr::get($response, 'expires_at'),
'id' => Arr::get($response, 'data.id') ?? Arr::get($response, 'id'),
];
}
@@ -97,4 +103,43 @@ class GiftVoucherCheckoutService
return $tier;
}
protected function ensureCustomerId(string $email): string
{
$payload = ['email' => $email];
try {
$response = $this->client->post('/customers', $payload);
} catch (PaddleException $exception) {
$customerId = $this->resolveExistingCustomerId($email, $exception);
if ($customerId) {
return $customerId;
}
throw $exception;
}
$customerId = Arr::get($response, 'data.id') ?? Arr::get($response, 'id');
if (! $customerId) {
throw new PaddleException('Failed to create Paddle customer.');
}
return $customerId;
}
protected function resolveExistingCustomerId(string $email, PaddleException $exception): ?string
{
if ($exception->status() !== 409 || Arr::get($exception->context(), 'error.code') !== 'customer_already_exists') {
return null;
}
$response = $this->client->get('/customers', [
'email' => $email,
'per_page' => 1,
]);
return Arr::get($response, 'data.0.id') ?? Arr::get($response, 'data.0.customer_id');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Services\Paddle;
use App\Models\Package;
use App\Models\Tenant;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
class PaddleCheckoutService
@@ -22,15 +21,6 @@ class PaddleCheckoutService
{
$customerId = $this->customers->ensureCustomerId($tenant);
$successUrl = $options['success_url'] ?? route('marketing.success', [
'locale' => App::getLocale(),
'packageId' => $package->id,
]);
$returnUrl = $options['return_url'] ?? route('packages', [
'locale' => App::getLocale(),
'highlight' => $package->slug,
]);
$customData = $this->buildMetadata(
$tenant,
$package,
@@ -46,21 +36,18 @@ class PaddleCheckoutService
],
],
'custom_data' => $customData,
'success_url' => $successUrl,
'cancel_url' => $returnUrl,
];
if (! empty($options['discount_id'])) {
$payload['discount_id'] = $options['discount_id'];
}
if ($tenant->contact_email) {
$payload['customer_email'] = $tenant->contact_email;
}
$response = $this->client->post('/transactions', $payload);
$response = $this->client->post('/checkout/links', $payload);
$checkoutUrl = Arr::get($response, 'data.url') ?? Arr::get($response, 'url');
$checkoutUrl = Arr::get($response, 'data.checkout.url')
?? Arr::get($response, 'checkout.url')
?? Arr::get($response, 'data.url')
?? Arr::get($response, 'url');
if (! $checkoutUrl) {
Log::warning('Paddle checkout response missing url', ['response' => $response]);
@@ -68,7 +55,9 @@ class PaddleCheckoutService
return [
'checkout_url' => $checkoutUrl,
'expires_at' => Arr::get($response, 'data.expires_at') ?? Arr::get($response, 'expires_at'),
'expires_at' => Arr::get($response, 'data.checkout.expires_at')
?? Arr::get($response, 'data.expires_at')
?? Arr::get($response, 'expires_at'),
'id' => Arr::get($response, 'data.id') ?? Arr::get($response, 'id'),
];
}