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(),
]);
}
}
}