HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux Bradford-Sitios 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64
User: www-data (33)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/middleware-citas-dev/app/Services/WebhookTokuService.php
<?php

namespace App\Services;

use App\Models\Appointment;
use App\Models\Config;
use App\Models\Payment;
use App\Services\BrevoService\BrevoMailer;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Log;

class WebhookTokuService
{
    public function process(array $payload): array
    {
        $eventType = data_get($payload, 'event_type', '');

        Log::info("[WebhookToku] Evento recibido", [
            'event_type' => $eventType,
            'transaction_id' => data_get($payload, 'transaction.id'),
            'external_id' => data_get($payload, 'transaction.external_id'),
            'amount' => data_get($payload, 'transaction.amount'),
            'status' => data_get($payload, 'transaction.status'),
        ]);

        try {
            switch ($eventType) {
                case 'transaction.success':
                    return $this->handleSuccess($payload);
                case 'transaction.failed':
                    return $this->handleFailed($payload);
                default:
                    Log::info("[WebhookToku] Evento no manejado", ['event_type' => $eventType]);
                    return ['handled' => false, 'reason' => 'Evento no manejado'];
            }
        } catch (\Exception $e) {
            Log::error("[WebhookToku] Error procesando webhook", [
                'event_type' => $eventType,
                'external_id' => data_get($payload, 'transaction.external_id'),
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            return ['handled' => false, 'reason' => $e->getMessage()];
        }
    }

    private function handleSuccess(array $payload): array
    {
        $transaction = data_get($payload, 'transaction', []);
        // El external_id puede venir a nivel transaction (checkout viejo) o en payment_intents[0].invoice_external_id (invoice nuevo)
        $externalId = data_get($transaction, 'external_id')
            ?: data_get($payload, 'payment_intents.0.invoice_external_id', '');
        $paymentInstrument = data_get($transaction, 'payment_instrument', data_get($payload, 'payment_instrument', []));

        Log::info("[WebhookToku][SUCCESS] Procesando pago exitoso", [
            'external_id' => $externalId,
            'toku_transaction_id' => data_get($transaction, 'id'),
            'amount' => data_get($transaction, 'amount'),
            'response_code' => data_get($transaction, 'response_code'),
            'card_brand' => data_get($paymentInstrument, 'card.card_brand'),
            'card_last_digits' => data_get($paymentInstrument, 'card.last_digits'),
        ]);

        // Buscar appointment por code
        $appointment = Appointment::where('code', $externalId)->first();

        if (!$appointment) {
            Log::warning("[WebhookToku][SUCCESS] Appointment no encontrado", ['external_id' => $externalId]);
            return ['handled' => false, 'reason' => 'Appointment no encontrado'];
        }

        Log::info("[WebhookToku][SUCCESS] Appointment encontrado", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'status_actual' => $appointment->status,
        ]);

        // Idempotencia: si ya está pagado, no procesar de nuevo
        if ($appointment->status === 'paid') {
            Log::info("[WebhookToku][SUCCESS] Appointment ya pagado, ignorando duplicado", [
                'appointment_id' => $appointment->id,
                'code' => $externalId,
            ]);
            return ['handled' => true, 'reason' => 'Ya procesado'];
        }

        // Crear registro de pago
        $payment = Payment::create([
            'appointment_id' => $appointment->id,
            'toku_transaction_id' => data_get($transaction, 'id'),
            'toku_invoice_id' => data_get($transaction, 'payment_intents.0.id_invoice'),
            'toku_customer_id' => data_get($payload, 'customer.id'),
            'invoice_external_id' => data_get($transaction, 'payment_intents.0.invoice_external_id', $externalId),
            'toku_event_id' => data_get($payload, 'id'),
            'event_type' => 'transaction.success',
            'status' => 'SUCCESS',
            'amount' => (int) data_get($transaction, 'amount', 0),
            'currency' => data_get($transaction, 'currency', 'CLP'),
            'transaction_date' => data_get($transaction, 'transaction_date'),
            'response_code' => data_get($transaction, 'response_code'),
            'response_message' => data_get($transaction, 'original_response_message'),
            'card_brand' => data_get($paymentInstrument, 'card.card_brand'),
            'card_type' => data_get($paymentInstrument, 'card.card_type'),
            'card_last_digits' => data_get($paymentInstrument, 'card.last_digits'),
            'card_holder' => data_get($paymentInstrument, 'card.card_holder'),
            'card_installments' => data_get($paymentInstrument, 'card.installments_number'),
            'raw_payload' => $payload,
        ]);

        Log::info("[WebhookToku][SUCCESS] Payment creado", [
            'payment_id' => $payment->id,
            'appointment_id' => $appointment->id,
            'toku_transaction_id' => $payment->toku_transaction_id,
            'amount' => $payment->amount,
        ]);

        // Marcar appointment como pagado
        $appointment->update(['status' => 'paid']);

        Log::info("[WebhookToku][SUCCESS] Appointment marcado como pagado", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'payment_id' => $payment->id,
        ]);

        // Crear account.payment en Odoo
        $this->createOdooPayment($appointment, $payment);

        // Generar PDF del comprobante (se reutiliza en ambos correos)
        $appointmentValue = Config::getValue('appointment_value', 35000);
        $pdf = Pdf::loadView('pdfs.payment-receipt', compact('appointment', 'payment', 'appointmentValue'));
        $pdfContent = base64_encode($pdf->output());

        // Enviar correo de confirmacion con comprobante PDF
        $this->sendPaymentConfirmationEmail($appointment, $payment, $pdfContent);

        // Notificar a administradores
        $this->sendAdminNotificationEmail($appointment, $payment, $pdfContent);

        return ['handled' => true, 'appointment_id' => $appointment->id, 'payment_id' => $payment->id];
    }

    private function handleFailed(array $payload): array
    {
        $transaction = data_get($payload, 'transaction', []);
        // El external_id puede venir a nivel transaction (checkout viejo) o en payment_intents[0].invoice_external_id (invoice nuevo)
        $externalId = data_get($transaction, 'external_id')
            ?: data_get($payload, 'payment_intents.0.invoice_external_id', '');
        $paymentInstrument = data_get($transaction, 'payment_instrument', data_get($payload, 'payment_instrument', []));

        $reason = data_get($transaction, 'original_response_message')
            ?? data_get($transaction, 'transaction_metadata.message')
            ?? 'Pago rechazado';

        Log::warning("[WebhookToku][FAILED] Procesando pago fallido", [
            'external_id' => $externalId,
            'toku_transaction_id' => data_get($transaction, 'id'),
            'amount' => data_get($transaction, 'amount'),
            'response_code' => data_get($transaction, 'response_code'),
            'reason' => $reason,
        ]);

        $appointment = Appointment::where('code', $externalId)->first();

        if (!$appointment) {
            Log::warning("[WebhookToku][FAILED] Appointment no encontrado", ['external_id' => $externalId]);
            return ['handled' => false, 'reason' => 'Appointment no encontrado'];
        }

        Log::info("[WebhookToku][FAILED] Appointment encontrado", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'status_actual' => $appointment->status,
        ]);

        // Idempotencia
        if (in_array($appointment->status, ['paid', 'failed'])) {
            Log::info("[WebhookToku][FAILED] Appointment ya procesado, ignorando duplicado", [
                'appointment_id' => $appointment->id,
                'code' => $externalId,
                'status' => $appointment->status,
            ]);
            return ['handled' => true, 'reason' => 'Ya procesado'];
        }

        // Crear registro de pago fallido
        $payment = Payment::create([
            'appointment_id' => $appointment->id,
            'toku_transaction_id' => data_get($transaction, 'id'),
            'toku_invoice_id' => data_get($transaction, 'payment_intents.0.id_invoice'),
            'toku_customer_id' => data_get($payload, 'customer.id'),
            'invoice_external_id' => data_get($transaction, 'payment_intents.0.invoice_external_id', $externalId),
            'toku_event_id' => data_get($payload, 'id'),
            'event_type' => 'transaction.failed',
            'status' => 'FAILED',
            'amount' => (int) data_get($transaction, 'amount', 0),
            'currency' => data_get($transaction, 'currency', 'CLP'),
            'transaction_date' => data_get($transaction, 'transaction_date'),
            'response_code' => data_get($transaction, 'response_code'),
            'response_message' => $reason,
            'card_brand' => data_get($paymentInstrument, 'card.card_brand'),
            'card_type' => data_get($paymentInstrument, 'card.card_type'),
            'card_last_digits' => data_get($paymentInstrument, 'card.last_digits'),
            'card_holder' => data_get($paymentInstrument, 'card.card_holder'),
            'card_installments' => data_get($paymentInstrument, 'card.installments_number'),
            'raw_payload' => $payload,
        ]);

        Log::warning("[WebhookToku][FAILED] Payment fallido creado", [
            'payment_id' => $payment->id,
            'appointment_id' => $appointment->id,
            'toku_transaction_id' => $payment->toku_transaction_id,
            'reason' => $reason,
        ]);

        // Marcar appointment como fallido
        $appointment->update(['status' => 'failed']);

        Log::warning("[WebhookToku][FAILED] Appointment marcado como fallido", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'payment_id' => $payment->id,
            'reason' => $reason,
        ]);

        return ['handled' => true, 'appointment_id' => $appointment->id, 'payment_id' => $payment->id];
    }

    private function createOdooPayment(Appointment $appointment, Payment $payment): void
    {
        try {
            $odoo = new OdooService();

            $odooPaymentId = $odoo->createAccountPayment([
                'payment_type' => 'inbound',
                'partner_type' => 'customer',
                'partner_id' => (int) $appointment->partner_id,
                'journal_id' => 22, // TOKU One Time
                'amount' => $payment->amount,
                'currency_id' => 45, // CLP
                'date' => now()->format('Y-m-d'),
                'ref' => 'Código Cita: ' . $appointment->code . ' - Código pago Toku: ' . $payment->toku_transaction_id,
            ]);

            // Confirmar el pago en Odoo si esta habilitado en config
            if (Config::getValue('odoo_confirm_payment', 'false') === 'true') {
                $odoo->executeKw('account.payment', 'action_post', [[$odooPaymentId]]);
                Log::info("[WebhookToku][SUCCESS] account.payment confirmado en Odoo", [
                    'odoo_payment_id' => $odooPaymentId,
                ]);
            }

            $payment->odoo_payment_id = $odooPaymentId;
            $payment->save();

            Log::info("[WebhookToku][SUCCESS] account.payment creado en Odoo", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'odoo_payment_id' => $odooPaymentId,
            ]);
        } catch (\Exception $e) {
            Log::error("[WebhookToku][SUCCESS] Error al crear account.payment en Odoo", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function sendPaymentConfirmationEmail(Appointment $appointment, Payment $payment, string $pdfContent): void
    {
        try {
            $email = $appointment->form_data['username'] ?? null;

            if (!$email) {
                Log::warning("[WebhookToku][EMAIL] No se encontro email para enviar confirmacion", [
                    'appointment_id' => $appointment->id,
                    'code' => $appointment->code,
                ]);
                return;
            }

            $odooSiteUrl = Config::getValue('odoo_site_url', '');
            $linkCitas = Config::getValue('link_citas', '/appointment');

            $emailData = [
                'name' => $appointment->form_data['name'] ?? '',
                'code' => $appointment->code,
                'transaction_id' => $payment->toku_transaction_id,
                'transaction_date' => $payment->transaction_date ? $payment->transaction_date->format('d/m/Y H:i:s') : $payment->created_at->format('d/m/Y H:i:s'),
                'card_brand' => $payment->card_brand,
                'card_type' => $payment->card_type,
                'card_last_digits' => $payment->card_last_digits,
                'card_installments' => $payment->card_installments,
                'amount' => $payment->amount,
                'currency' => $payment->currency,
                'link_citas' => rtrim($odooSiteUrl, '/') . $linkCitas,
            ];

            $attachments = [
                [
                    'name' => "comprobante_pago_{$appointment->code}.pdf",
                    'content' => $pdfContent,
                    'base64' => 1,
                ],
            ];

            BrevoMailer::send(
                $email,
                'Confirmacion de pago - ' . $appointment->code,
                'templates_email.email_payment_confirmation',
                ['data' => $emailData],
                $attachments
            );

            Log::info("[WebhookToku][EMAIL] Correo de confirmacion enviado", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'email' => $email,
            ]);
        } catch (\Exception $e) {
            Log::error("[WebhookToku][EMAIL] Error al enviar correo de confirmacion", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function sendAdminNotificationEmail(Appointment $appointment, Payment $payment, string $pdfContent): void
    {
        try {
            $adminEmails = Config::getValue('admin_notification_emails', '');

            if (empty($adminEmails)) {
                Log::info("[WebhookToku][ADMIN_EMAIL] No hay correos de administradores configurados");
                return;
            }

            $recipients = array_filter(array_map('trim', explode(',', $adminEmails)));

            if (empty($recipients)) {
                return;
            }

            $emailData = [
                'name' => $appointment->form_data['name'] ?? '',
                'email' => $appointment->form_data['username'] ?? '',
                'postulacion_id' => $appointment->postulacion_id,
                'code' => $appointment->code,
                'transaction_date' => $payment->transaction_date ? $payment->transaction_date->format('d/m/Y H:i:s') : $payment->created_at->format('d/m/Y H:i:s'),
                'card_brand' => $payment->card_brand,
                'card_type' => $payment->card_type,
                'card_last_digits' => $payment->card_last_digits,
                'amount' => $payment->amount,
                'currency' => $payment->currency,
                'odoo_payment_id' => $payment->odoo_payment_id,
            ];

            $attachments = [
                [
                    'name' => "comprobante_pago_{$appointment->code}.pdf",
                    'content' => $pdfContent,
                    'base64' => 1,
                ],
            ];

            foreach ($recipients as $adminEmail) {
                BrevoMailer::send(
                    $adminEmail,
                    'Nuevo pago recibido - ' . $appointment->code,
                    'templates_email.email_admin_payment_notification',
                    ['data' => $emailData],
                    $attachments
                );
            }

            Log::info("[WebhookToku][ADMIN_EMAIL] Notificacion enviada a administradores", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'recipients' => $recipients,
            ]);
        } catch (\Exception $e) {
            Log::error("[WebhookToku][ADMIN_EMAIL] Error al enviar notificacion a administradores", [
                'appointment_id' => $appointment->id,
                'code' => $appointment->code,
                'error' => $e->getMessage(),
            ]);
        }
    }

}