user(); $tenant = $user?->tenant; if (! $tenant) { abort(404); } return Inertia::render('marketing/WithdrawalConfirm', [ 'eligiblePurchases' => $this->eligiblePurchases($tenant, $locale), 'windowDays' => self::WITHDRAWAL_DAYS, ]); } public function confirm( WithdrawalConfirmRequest $request, PaddleTransactionService $transactions, string $locale ): RedirectResponse { $user = $request->user(); $tenant = $user?->tenant; if (! $tenant) { abort(404); } $purchaseId = $request->integer('purchase_id'); $purchase = PackagePurchase::query() ->with('package') ->where('tenant_id', $tenant->id) ->findOrFail($purchaseId); $eligibility = $this->evaluateEligibility($purchase, $tenant); if (! $eligibility['eligible']) { return redirect() ->back() ->with('error', __('marketing.withdrawal.errors.not_eligible', [], $locale)); } $transactionId = $this->resolveTransactionId($purchase); if (! $transactionId) { Log::warning('Withdrawal missing Paddle transaction reference.', [ 'purchase_id' => $purchase->id, 'provider' => $purchase->provider, ]); return redirect() ->back() ->with('error', __('marketing.withdrawal.errors.missing_transaction', [], $locale)); } try { $transactions->refund($transactionId, ['reason' => 'withdrawal']); } catch (\Throwable $exception) { Log::warning('Withdrawal refund failed', [ 'purchase_id' => $purchase->id, 'transaction_id' => $transactionId, 'error' => $exception->getMessage(), ]); return redirect() ->back() ->with('error', __('marketing.withdrawal.errors.refund_failed', [], $locale)); } $confirmedAt = now(); $metadata = $purchase->metadata ?? []; $withdrawalMeta = is_array($metadata['withdrawal'] ?? null) ? $metadata['withdrawal'] : []; $withdrawalMeta = array_merge($withdrawalMeta, [ 'confirmed_at' => $confirmedAt->toIso8601String(), 'confirmed_by' => $user?->id, 'transaction_id' => $transactionId, ]); $metadata['withdrawal'] = $withdrawalMeta; $purchase->forceFill([ 'provider_id' => $transactionId, 'refunded' => true, 'metadata' => $metadata, ])->save(); $this->deactivateTenantPackage($tenant, $purchase); $recipient = $tenant->contact_email ?? $user?->email; if ($recipient) { Notification::route('mail', $recipient) ->notify(new WithdrawalConfirmed($purchase, $confirmedAt)); } return redirect() ->back() ->with('success', __('marketing.withdrawal.success', [], $locale)); } /** * @return array> */ private function eligiblePurchases(Tenant $tenant, string $locale): array { $purchases = PackagePurchase::query() ->with('package') ->where('tenant_id', $tenant->id) ->where('type', 'endcustomer_event') ->where('provider', 'paddle') ->where('refunded', false) ->orderByDesc('purchased_at') ->orderByDesc('id') ->get(); return $purchases ->filter(fn (PackagePurchase $purchase) => $this->evaluateEligibility($purchase, $tenant)['eligible']) ->map(fn (PackagePurchase $purchase) => $this->mapPurchaseForView($purchase, $locale)) ->values() ->all(); } /** * @return array{eligible: bool, reasons: array} */ private function evaluateEligibility(PackagePurchase $purchase, Tenant $tenant): array { $reasons = []; if ($purchase->type !== 'endcustomer_event') { $reasons[] = 'type'; } if ($purchase->provider !== 'paddle') { $reasons[] = 'provider'; } if ($purchase->refunded) { $reasons[] = 'refunded'; } if (! $this->resolveTransactionId($purchase)) { $reasons[] = 'missing_reference'; } if (! $this->isWithinWindow($purchase)) { $reasons[] = 'expired'; } if ($this->hasAttachedEvent($purchase, $tenant)) { $reasons[] = 'event_used'; } return [ 'eligible' => $reasons === [], 'reasons' => $reasons, ]; } private function isWithinWindow(PackagePurchase $purchase): bool { $purchasedAt = $purchase->purchased_at; if (! $purchasedAt) { return false; } return $purchasedAt->greaterThanOrEqualTo(now()->subDays(self::WITHDRAWAL_DAYS)); } private function hasAttachedEvent(PackagePurchase $purchase, Tenant $tenant): bool { if (! $purchase->purchased_at) { return true; } return EventPackage::query() ->where('package_id', $purchase->package_id) ->where('purchased_at', '>=', $purchase->purchased_at) ->whereHas('event', fn ($query) => $query->where('tenant_id', $tenant->id)) ->exists(); } /** * @return array */ private function mapPurchaseForView(PackagePurchase $purchase, string $locale): array { $package = $purchase->package; $currency = data_get($purchase->metadata, 'currency', 'EUR'); $purchasedAt = $purchase->purchased_at; $expiresAt = $purchasedAt?->copy()->addDays(self::WITHDRAWAL_DAYS); $packageName = $package?->getNameForLocale($locale) ?? $package?->name ?? __('emails.package_limits.package_fallback', [], $locale); return [ 'id' => $purchase->id, 'package_name' => $packageName, 'purchased_at' => $purchasedAt?->toIso8601String(), 'expires_at' => $expiresAt?->toIso8601String(), 'price' => $purchase->price, 'currency' => $currency, ]; } private function resolveTransactionId(PackagePurchase $purchase): ?string { if ($purchase->provider === 'paddle' && $purchase->provider_id) { return (string) $purchase->provider_id; } return data_get($purchase->metadata, 'paddle_transaction_id'); } private function deactivateTenantPackage(Tenant $tenant, PackagePurchase $purchase): void { $tenant->tenantPackages() ->where('package_id', $purchase->package_id) ->where('active', true) ->update([ 'active' => false, 'expires_at' => now(), ]); $hasActive = $tenant->tenantPackages() ->where('active', true) ->whereHas('package', fn ($query) => $query->where('type', 'endcustomer')) ->exists(); if (! $hasActive) { $tenant->forceFill([ 'subscription_status' => 'free', 'subscription_expires_at' => null, ])->save(); } } }