Updated checkout to wait for backend confirmation before advancing, added a “Processing payment…” state with retry/ refresh fallback, and now use Paddle totals/currency for purchase records + confirmation emails (with new email translations).

This commit is contained in:
Codex Agent
2025-12-22 09:06:48 +01:00
parent 41d29eb7d3
commit 84234bfb8e
36 changed files with 1742 additions and 187 deletions

View File

@@ -3,7 +3,6 @@
namespace App\Services\Checkout;
use App\Mail\PurchaseConfirmation;
use App\Mail\Welcome;
use App\Models\AbandonedCheckout;
use App\Models\CheckoutSession;
use App\Models\Package;
@@ -13,6 +12,7 @@ use App\Models\TenantPackage;
use App\Models\User;
use App\Notifications\Ops\PurchaseCreated;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
@@ -69,6 +69,10 @@ class CheckoutAssignmentService
?? ($metadata['paddle_transaction_id'] ?? $metadata['paddle_checkout_id'] ? CheckoutSession::PROVIDER_PADDLE : null)
?? CheckoutSession::PROVIDER_FREE;
$totals = $this->resolvePaddleTotals($session, $options['payload'] ?? []);
$currency = $totals['currency'] ?? $session->currency ?? $package->currency ?? 'EUR';
$price = array_key_exists('total', $totals) ? $totals['total'] : (float) $session->amount_total;
$purchase = PackagePurchase::updateOrCreate(
[
'tenant_id' => $tenant->id,
@@ -77,30 +81,39 @@ class CheckoutAssignmentService
],
[
'provider' => $providerName,
'price' => $session->amount_total,
'price' => round($price, 2),
'type' => $package->type === 'reseller' ? 'reseller_subscription' : 'endcustomer_event',
'purchased_at' => now(),
'metadata' => array_filter([
'payload' => $options['payload'] ?? null,
'checkout_session_id' => $session->id,
'consents' => $consents ?: null,
]),
'paddle_totals' => $totals !== [] ? $totals : null,
'currency' => $currency,
], static fn ($value) => $value !== null && $value !== ''),
]
);
TenantPackage::updateOrCreate(
$tenantPackage = TenantPackage::updateOrCreate(
[
'tenant_id' => $tenant->id,
'package_id' => $package->id,
],
[
'price' => $session->amount_total,
'price' => round($price, 2),
'active' => true,
'purchased_at' => now(),
'expires_at' => $this->resolveExpiry($package, $tenant),
]
);
if ($package->type !== 'reseller') {
$tenant->forceFill([
'subscription_status' => 'active',
'subscription_expires_at' => $tenantPackage->expires_at,
])->save();
}
if ($user && $user->pending_purchase) {
$this->activateUser($user);
}
@@ -108,10 +121,6 @@ class CheckoutAssignmentService
if ($user) {
$mailLocale = $user->preferred_locale ?? app()->getLocale();
Mail::to($user)
->locale($mailLocale)
->queue(new Welcome($user));
if ($purchase->wasRecentlyCreated) {
Mail::to($user)
->locale($mailLocale)
@@ -196,4 +205,63 @@ class CheckoutAssignmentService
'pending_purchase' => false,
])->save();
}
/**
* @param array<string, mixed> $payload
* @return array{currency?: string, subtotal?: float, discount?: float, tax?: float, total?: float}
*/
protected function resolvePaddleTotals(CheckoutSession $session, array $payload): array
{
$metadataTotals = $session->provider_metadata['paddle_totals'] ?? null;
if (is_array($metadataTotals) && $metadataTotals !== []) {
return $metadataTotals;
}
$totals = Arr::get($payload, 'details.totals', Arr::get($payload, 'totals', []));
if (! is_array($totals) || $totals === []) {
return [];
}
$currency = Arr::get($totals, 'currency_code')
?? Arr::get($payload, 'currency_code')
?? Arr::get($totals, 'currency')
?? Arr::get($payload, 'currency');
$subtotal = $this->convertMinorAmount(Arr::get($totals, 'subtotal.amount', $totals['subtotal'] ?? null));
$discount = $this->convertMinorAmount(Arr::get($totals, 'discount.amount', $totals['discount'] ?? null));
$tax = $this->convertMinorAmount(Arr::get($totals, 'tax.amount', $totals['tax'] ?? null));
$total = $this->convertMinorAmount(
Arr::get(
$totals,
'total.amount',
$totals['total'] ?? Arr::get($totals, 'grand_total.amount', $totals['grand_total'] ?? null)
)
);
return array_filter([
'currency' => $currency ? strtoupper((string) $currency) : null,
'subtotal' => $subtotal,
'discount' => $discount,
'tax' => $tax,
'total' => $total,
], static fn ($value) => $value !== null);
}
protected function convertMinorAmount(mixed $value): ?float
{
if ($value === null || $value === '') {
return null;
}
if (is_array($value) && isset($value['amount'])) {
$value = $value['amount'];
}
if (! is_numeric($value)) {
return null;
}
return round(((float) $value) / 100, 2);
}
}