resolvePriceId($payload); $amount = $this->resolveAmount($payload); $currency = Str::upper($this->resolveCurrency($payload)); $expiresAt = now()->addYears((int) config('gift-vouchers.default_valid_years', 5)); $voucher = GiftVoucher::query()->updateOrCreate( [ 'paddle_transaction_id' => $payload['id'] ?? null, ], [ 'code' => $metadata['gift_code'] ?? $this->generateCode(), 'amount' => $amount, 'currency' => $currency, 'status' => GiftVoucher::STATUS_ISSUED, 'purchaser_email' => $metadata['purchaser_email'] ?? Arr::get($payload, 'customer.email'), 'recipient_email' => $metadata['recipient_email'] ?? null, 'recipient_name' => $metadata['recipient_name'] ?? null, 'message' => $metadata['message'] ?? null, 'paddle_checkout_id' => $payload['checkout_id'] ?? Arr::get($payload, 'details.checkout_id'), 'paddle_price_id' => $priceId, 'metadata' => $metadata, 'expires_at' => $expiresAt, 'refunded_at' => null, 'redeemed_at' => null, ] ); if (! $voucher->coupon_id) { $coupon = $this->createCouponForVoucher($voucher); $voucher->forceFill(['coupon_id' => $coupon->id])->save(); SyncCouponToPaddle::dispatch($coupon); } return $voucher; } public function markRedeemed(?Coupon $coupon, ?string $transactionId = null): void { if (! $coupon?->giftVoucher) { return; } $voucher = $coupon->giftVoucher; if ($voucher->isRedeemed()) { return; } $voucher->forceFill([ 'status' => GiftVoucher::STATUS_REDEEMED, 'redeemed_at' => now(), 'metadata' => array_merge($voucher->metadata ?? [], array_filter([ 'redeemed_transaction_id' => $transactionId, ])), ])->save(); } /** * @return array */ public function refund(GiftVoucher $voucher, ?string $reason = null): array { if (! $voucher->canBeRefunded()) { throw ValidationException::withMessages([ 'voucher' => __('Voucher cannot be refunded after redemption or refund.'), ]); } if (! $voucher->paddle_transaction_id) { throw ValidationException::withMessages([ 'voucher' => __('Missing Paddle transaction for refund.'), ]); } $response = $this->transactions->refund($voucher->paddle_transaction_id, array_filter([ 'reason' => $reason, ])); $voucher->forceFill([ 'status' => GiftVoucher::STATUS_REFUNDED, 'refunded_at' => now(), ])->save(); if ($voucher->coupon) { $voucher->coupon->forceFill([ 'status' => CouponStatus::ARCHIVED, 'enabled_for_checkout' => false, ])->save(); } return $response; } protected function createCouponForVoucher(GiftVoucher $voucher): Coupon { $packages = $this->eligiblePackages(); $coupon = Coupon::create([ 'name' => 'Gutschein '.$voucher->code, 'code' => $voucher->code, 'type' => CouponType::FLAT, 'amount' => $voucher->amount, 'currency' => $voucher->currency, 'status' => CouponStatus::ACTIVE, 'enabled_for_checkout' => true, 'is_stackable' => false, 'usage_limit' => 1, 'per_customer_limit' => 1, 'auto_apply' => false, 'description' => 'Geschenkgutschein '.number_format((float) $voucher->amount, 2).' '.$voucher->currency.' für Endkunden-Pakete.', 'starts_at' => now(), 'ends_at' => $voucher->expires_at, ]); if ($packages->isNotEmpty()) { $coupon->packages()->sync($packages->pluck('id')); } return $coupon; } protected function eligiblePackages(): Collection { $types = (array) config('gift-vouchers.package_types', ['endcustomer']); return Package::query() ->whereIn('type', $types) ->whereNotNull('paddle_price_id') ->get(['id']); } protected function resolvePriceId(array $payload): ?string { $metadata = $payload['metadata'] ?? []; if (is_array($metadata) && ! empty($metadata['paddle_price_id'])) { return $metadata['paddle_price_id']; } $items = Arr::get($payload, 'items', Arr::get($payload, 'details.items', [])); if (is_array($items) && isset($items[0]['price_id'])) { return $items[0]['price_id']; } return $payload['price_id'] ?? null; } protected function resolveAmount(array $payload): float { $tiers = Collection::make(config('gift-vouchers.tiers', [])) ->keyBy(fn ($tier) => $tier['paddle_price_id'] ?? null); $priceId = $this->resolvePriceId($payload); if ($priceId && $tiers->has($priceId)) { return (float) $tiers->get($priceId)['amount']; } $amount = Arr::get($payload, 'totals.grand_total.amount') ?? Arr::get($payload, 'totals.grand_total') ?? Arr::get($payload, 'details.totals.grand_total.amount') ?? Arr::get($payload, 'details.totals.grand_total') ?? Arr::get($payload, 'amount'); if (is_numeric($amount)) { $value = (float) $amount; return $value >= 100 ? round($value / 100, 2) : round($value, 2); } Log::warning('[GiftVoucher] Unable to resolve amount, defaulting to 0', ['payload' => $payload]); return 0.0; } protected function resolveCurrency(array $payload): string { return $payload['currency_code'] ?? Arr::get($payload, 'details.totals.currency_code') ?? Arr::get($payload, 'currency') ?? 'EUR'; } protected function generateCode(): string { return 'GIFT-'.Str::upper(Str::random(8)); } }