Files
fotospiel-app/app/Services/Addons/EventAddonWebhookService.php
Codex Agent 10c99de1e2
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Migrate billing from Paddle to Lemon Squeezy
2026-02-03 10:59:54 +01:00

179 lines
6.0 KiB
PHP

<?php
namespace App\Services\Addons;
use App\Models\EventPackage;
use App\Models\EventPackageAddon;
use App\Notifications\Addons\AddonPurchaseReceipt;
use App\Notifications\Ops\AddonPurchased;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
class EventAddonWebhookService
{
public function __construct(private readonly EventAddonCatalog $catalog) {}
public function handle(array $payload): bool
{
$eventType = $payload['meta']['event_name'] ?? null;
$data = $payload['data'] ?? [];
if (! in_array($eventType, ['order_created', 'order_updated'], true)) {
return false;
}
$status = strtolower((string) data_get($data, 'attributes.status', ''));
if ($status !== 'paid') {
return false;
}
$metadata = $this->extractMetadata($payload);
$intentId = $metadata['addon_intent'] ?? null;
$addonKey = $metadata['addon_key'] ?? null;
if (! $intentId || ! $addonKey) {
return false;
}
$transactionId = $data['id'] ?? null;
$checkoutId = data_get($data, 'attributes.checkout_id') ?? null;
$addon = EventPackageAddon::query()
->where('addon_key', $addonKey)
->where(function ($query) use ($intentId, $checkoutId, $transactionId) {
$query->where('metadata->addon_intent', $intentId)
->orWhere('checkout_id', $checkoutId)
->orWhere('transaction_id', $transactionId);
})
->latest('id')
->first();
if (! $addon) {
Log::info('[AddonWebhook] Add-on intent not found', [
'intent' => $intentId,
'addon_key' => $addonKey,
'transaction_id' => $transactionId,
]);
return false;
}
if ($addon->status === 'completed') {
return true; // idempotent
}
$increments = $this->resolveAddonIncrements($addon, $addonKey);
DB::transaction(function () use ($addon, $transactionId, $checkoutId, $data, $increments) {
$addon->forceFill([
'transaction_id' => $transactionId,
'checkout_id' => $addon->checkout_id ?: $checkoutId,
'status' => 'completed',
'amount' => $this->resolveAmount($data),
'currency' => Arr::get($data, 'attributes.currency') ?? Arr::get($data, 'currency'),
'metadata' => array_merge($addon->metadata ?? [], ['webhook_payload' => $data]),
'receipt_payload' => Arr::get($data, 'attributes.urls.receipt')
? ['receipt_url' => Arr::get($data, 'attributes.urls.receipt')]
: null,
'purchased_at' => now(),
])->save();
/** @var EventPackage $eventPackage */
$eventPackage = EventPackage::query()
->lockForUpdate()
->find($addon->event_package_id);
if (! $eventPackage) {
return;
}
$eventPackage->forceFill([
'extra_photos' => ($eventPackage->extra_photos ?? 0) + (int) ($increments['extra_photos'] ?? 0) * $addon->quantity,
'extra_guests' => ($eventPackage->extra_guests ?? 0) + (int) ($increments['extra_guests'] ?? 0) * $addon->quantity,
'extra_gallery_days' => ($eventPackage->extra_gallery_days ?? 0) + (int) ($increments['extra_gallery_days'] ?? 0) * $addon->quantity,
]);
if (($increments['extra_gallery_days'] ?? 0) > 0) {
$base = $eventPackage->gallery_expires_at ?? now();
$eventPackage->gallery_expires_at = $base->copy()->addDays((int) ($increments['extra_gallery_days'] ?? 0) * $addon->quantity);
}
$eventPackage->save();
$tenant = $addon->tenant;
if ($tenant) {
Notification::route('mail', [$tenant->contact_email ?? $tenant->user?->email])
->notify(new AddonPurchaseReceipt($addon));
$opsEmail = config('mail.ops_address');
if ($opsEmail) {
Notification::route('mail', $opsEmail)->notify(new AddonPurchased($addon));
}
}
});
return true;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function extractMetadata(array $data): array
{
$metadata = [];
if (isset($data['meta']['custom_data']) && is_array($data['meta']['custom_data'])) {
$metadata = $data['meta']['custom_data'];
}
if (isset($data['metadata']) && is_array($data['metadata'])) {
$metadata = array_merge($metadata, $data['metadata']);
}
if (isset($data['attributes']['custom_data']) && is_array($data['attributes']['custom_data'])) {
$metadata = array_merge($metadata, $data['attributes']['custom_data']);
}
return $metadata;
}
private function resolveAmount(array $data): ?float
{
$total = Arr::get($data, 'attributes.total');
if ($total === null || $total === '') {
return null;
}
if (! is_numeric($total)) {
return null;
}
return round(((float) $total) / 100, 2);
}
/**
* @return array<string, int>
*/
private function resolveAddonIncrements(EventPackageAddon $addon, string $addonKey): array
{
$stored = Arr::get($addon->metadata ?? [], 'increments', []);
if (is_array($stored) && $stored !== []) {
$filtered = collect($stored)
->map(fn ($value) => is_numeric($value) ? (int) $value : 0)
->filter(fn ($value) => $value > 0)
->all();
if ($filtered !== []) {
return $filtered;
}
}
return $this->catalog->resolveIncrements($addonKey);
}
}