a:3:{s:5:"queue";s:9:"campaigns";s:4:"then";a:1:{i:0;O:47:"Laravel\SerializableClosure\SerializableClosure":1:{s:12:"serializable";O:46:"Laravel\SerializableClosure\Serializers\Signed":2:{s:12:"serializable";s:3353:"O:46:"Laravel\SerializableClosure\Serializers\Native":5:{s:3:"use";a:3:{s:8:"campaign";O:45:"Illuminate\Contracts\Database\ModelIdentifier":5:{s:5:"class";s:19:"App\Models\Campaign";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:5:"mysql";s:15:"collectionClass";N;}s:7:"machine";O:33:"App\Services\CampaignStateMachine":0:{}s:7:"billing";O:35:"App\Services\Billing\BillingService":1:{s:44:"App\Services\Billing\BillingServicepricing";O:35:"App\Services\Billing\PricingService":0:{}}}s:8:"function";s:2723:"function () use ($campaign, $machine, $billing) {
                // P0R-07: refund calculation under pessimistic lock + idempotency key.
                // Antes: lia sent_count fora de transação — race entre 2 callbacks
                // (cancel + then ou retry de batch) podia liberar 2x.
                // Agora: tudo numa transação curta, com refund_done flag em settings.
                \Illuminate\Support\Facades\DB::transaction(function () use ($campaign, $billing) {
                    $fresh = \App\Models\Campaign::withoutGlobalScopes()
                        ->where('id', $campaign->id)
                        ->lockForUpdate()
                        ->first();
                    if (! $fresh) return;
                    $snapshot = $fresh->settings ?? [];
                    if (! empty($snapshot['refund_done'])) {
                        return; // refund já feito por outra rota (cancel/catch)
                    }
                    $unitCents = (int) ($fresh->unit_cents_at_dispatch ?? $snapshot['unit_cents_at_dispatch'] ?? 0);
                    $reserved  = (int) ($fresh->reserved_cents ?? $snapshot['reserved_cents'] ?? 0);
                    $consumed  = $unitCents * max(0, (int) $fresh->sent_count);
                    $refund    = max(0, $reserved - $consumed);
                    if ($refund > 0) {
                        $billing->release(
                            $fresh->tenant_id,
                            $refund,
                            'campaign_dispatch_refund',
                            $fresh->id
                        );
                    }
                    $fresh->forceFill([
                        'settings' => array_merge($snapshot, ['refund_done' => true]),
                    ])->save();
                });
                $campaign->refresh();
                $machine->transition($campaign, 'completed');

                // Notifications
                try {
                    $admin = \App\Models\User::where('tenant_id', $campaign->tenant_id)->first();
                    if ($admin) {
                        $admin->notify(new \App\Notifications\CampaignCompletedNotification($campaign));

                        $balanceCents = (int) ($campaign->tenant?->balance_cents ?? 0);
                        if ($balanceCents > 0 && $balanceCents < 10000) {
                            $admin->notify(new \App\Notifications\LowCreditsNotification($balanceCents));
                        }
                    }
                } catch (\Throwable $e) {
                    \Illuminate\Support\Facades\Log::channel('campaign')->warning('notification.failed', ['error' => $e->getMessage()]);
                }
            }";s:5:"scope";s:27:"App\Jobs\ProcessCampaignJob";s:4:"this";N;s:4:"self";s:32:"00000000000007ef0000000000000000";}";s:4:"hash";s:44:"AIK1k2K30iArrXovgzTFwLIri92Xn/O+SJ85d3QDkEQ=";}}}s:5:"catch";a:1:{i:0;O:47:"Laravel\SerializableClosure\SerializableClosure":1:{s:12:"serializable";O:46:"Laravel\SerializableClosure\Serializers\Signed":2:{s:12:"serializable";s:2399:"O:46:"Laravel\SerializableClosure\Serializers\Native":5:{s:3:"use";a:3:{s:8:"campaign";O:45:"Illuminate\Contracts\Database\ModelIdentifier":5:{s:5:"class";s:19:"App\Models\Campaign";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:5:"mysql";s:15:"collectionClass";N;}s:7:"machine";O:33:"App\Services\CampaignStateMachine":0:{}s:7:"billing";O:35:"App\Services\Billing\BillingService":1:{s:44:"App\Services\Billing\BillingServicepricing";O:35:"App\Services\Billing\PricingService":0:{}}}s:8:"function";s:1769:"function (\Throwable $e) use ($campaign, $machine, $billing) {
                // P0R-07: mesma lógica idempotente sob lock do then().
                \Illuminate\Support\Facades\DB::transaction(function () use ($campaign, $billing) {
                    $fresh = \App\Models\Campaign::withoutGlobalScopes()
                        ->where('id', $campaign->id)
                        ->lockForUpdate()
                        ->first();
                    if (! $fresh) return;
                    $snapshot = $fresh->settings ?? [];
                    if (! empty($snapshot['refund_done'])) return;
                    $unitCents = (int) ($fresh->unit_cents_at_dispatch ?? $snapshot['unit_cents_at_dispatch'] ?? 0);
                    $reserved  = (int) ($fresh->reserved_cents ?? $snapshot['reserved_cents'] ?? 0);
                    $consumed  = $unitCents * max(0, (int) $fresh->sent_count);
                    $refund    = max(0, $reserved - $consumed);
                    if ($refund > 0) {
                        $billing->release(
                            $fresh->tenant_id,
                            $refund,
                            'campaign_dispatch_refund',
                            $fresh->id
                        );
                    }
                    $fresh->forceFill([
                        'settings' => array_merge($snapshot, ['refund_done' => true]),
                    ])->save();
                });
                $campaign->refresh();
                $machine->transition($campaign, 'failed');
                \Illuminate\Support\Facades\Log::channel('campaign')->error('campaign.batch.failed', [
                    'campaign_id' => $campaign->id, 'error' => $e->getMessage(),
                ]);
            }";s:5:"scope";s:27:"App\Jobs\ProcessCampaignJob";s:4:"this";N;s:4:"self";s:32:"00000000000007d10000000000000000";}";s:4:"hash";s:44:"YPofkOomGklHleIskcCQik5EwtmH9xB04o4wdUg+aDI=";}}}}