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

@@ -115,6 +115,7 @@ class CheckoutWebhookService
return true;
case 'transaction.completed':
$this->syncSessionTotals($session, $data);
if ($session->status !== CheckoutSession::STATUS_COMPLETED) {
$this->sessions->markProcessing($session, [
'paddle_status' => $status ?: 'completed',
@@ -146,6 +147,87 @@ class CheckoutWebhookService
}
}
protected function syncSessionTotals(CheckoutSession $session, array $data): void
{
$totals = $this->normalizePaddleTotals($data);
if ($totals === []) {
return;
}
$updates = [];
if (array_key_exists('subtotal', $totals)) {
$updates['amount_subtotal'] = $totals['subtotal'];
}
if (array_key_exists('discount', $totals)) {
$updates['amount_discount'] = $totals['discount'];
}
if (array_key_exists('total', $totals)) {
$updates['amount_total'] = $totals['total'];
}
if (! empty($totals['currency'])) {
$updates['currency'] = $totals['currency'];
}
if ($updates !== []) {
$session->forceFill($updates)->save();
}
$this->mergeProviderMetadata($session, [
'paddle_totals' => $totals,
]);
}
/**
* @return array{currency?: string, subtotal?: float, discount?: float, tax?: float, total?: float}
*/
protected function normalizePaddleTotals(array $data): array
{
$totals = Arr::get($data, 'details.totals', Arr::get($data, 'totals', []));
$currency = Arr::get($totals, 'currency_code')
?? $data['currency_code'] ?? Arr::get($totals, 'currency') ?? Arr::get($data, '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);
}
protected function handlePaddleSubscriptionEvent(string $eventType, array $data): bool
{
$subscriptionId = $data['id'] ?? null;
@@ -154,8 +236,8 @@ class CheckoutWebhookService
return false;
}
$metadata = $data['metadata'] ?? [];
$tenant = $this->resolveTenantFromSubscription($data, $metadata, $subscriptionId);
$customData = $this->extractCustomData($data);
$tenant = $this->resolveTenantFromSubscription($data, $customData, $subscriptionId);
if (! $tenant) {
Log::info('[CheckoutWebhook] Paddle subscription tenant not resolved', [
@@ -165,7 +247,7 @@ class CheckoutWebhookService
return false;
}
$package = $this->resolvePackageFromSubscription($data, $metadata, $subscriptionId);
$package = $this->resolvePackageFromSubscription($data, $customData, $subscriptionId);
if (! $package) {
Log::info('[CheckoutWebhook] Paddle subscription package not resolved', [
@@ -317,7 +399,7 @@ class CheckoutWebhookService
protected function isGiftVoucherEvent(array $data): bool
{
$metadata = $data['metadata'] ?? [];
$metadata = $this->extractCustomData($data);
$type = is_array($metadata) ? ($metadata['type'] ?? $metadata['kind'] ?? $metadata['category'] ?? null) : null;
@@ -336,7 +418,7 @@ class CheckoutWebhookService
protected function locatePaddleSession(array $data): ?CheckoutSession
{
$metadata = $data['metadata'] ?? [];
$metadata = $this->extractCustomData($data);
if (is_array($metadata)) {
$sessionId = $metadata['checkout_session_id'] ?? null;
@@ -372,4 +454,27 @@ class CheckoutWebhookService
return null;
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function extractCustomData(array $data): array
{
$customData = [];
if (isset($data['custom_data']) && is_array($data['custom_data'])) {
$customData = $data['custom_data'];
}
if (isset($data['customData']) && is_array($data['customData'])) {
$customData = array_merge($customData, $data['customData']);
}
if (isset($data['metadata']) && is_array($data['metadata'])) {
$customData = array_merge($customData, $data['metadata']);
}
return $customData;
}
}