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/TokuInvoiceService.php
<?php

namespace App\Services;

use App\Models\Appointment;
use App\Models\TokuCustomer;
use Illuminate\Support\Facades\Log;

class TokuInvoiceService
{
    private ApiTokuService $api;

    public function __construct()
    {
        $this->api = new ApiTokuService();
    }

    /**
     * Crea (o reutiliza) customer y subscription en Toku, luego crea un invoice
     * para la cita y devuelve la URL del portal de pago.
     *
     * @return array{customer_id:string, subscription_id:string, invoice_id:string, invoice_external_id:string, url:string}
     */
    public function createInvoiceForAppointment(Appointment $appointment, int $amount, array $odooPartner): array
    {
        $customer = $this->getOrCreateCustomer($appointment, $odooPartner);
        $subscriptionId = $this->getOrCreateSubscription($customer, $appointment);

        $invoice = $this->createInvoice($customer, $subscriptionId, $appointment, $amount);

        $portalUrl = $this->buildPortalUrl($customer->code_toku, $invoice['id']);

        Log::info("[TokuInvoice] Invoice creado y URL armada", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'customer_id' => $customer->code_toku,
            'subscription_id' => $subscriptionId,
            'invoice_id' => $invoice['id'],
            'portal_url' => $portalUrl,
        ]);

        return [
            'customer_id' => $customer->code_toku,
            'subscription_id' => $subscriptionId,
            'invoice_id' => $invoice['id'],
            'invoice_external_id' => $appointment->code,
            'url' => $portalUrl,
        ];
    }

    private function getOrCreateCustomer(Appointment $appointment, array $odooPartner): TokuCustomer
    {
        $customer = TokuCustomer::where('partner_id', $appointment->partner_id)->first();

        if ($customer && !empty($customer->code_toku)) {
            Log::info("[TokuInvoice] Customer reutilizado", [
                'partner_id' => $customer->partner_id,
                'code_toku' => $customer->code_toku,
            ]);
            return $customer;
        }

        $rutRaw = $odooPartner['vat'] ?? ($appointment->form_data['rut'] ?? null);
        $email = $odooPartner['email'] ?? ($appointment->form_data['username'] ?? null);
        $name = $odooPartner['name'] ?? ($appointment->form_data['name'] ?? '');
        $phone = $odooPartner['mobile'] ?? $odooPartner['phone'] ?? null;

        $governmentId = $rutRaw ? $this->formatGovernmentId($rutRaw) : env('TOKU_GENERIC_RUT', '111111111');

        $payload = [
            'government_id' => $governmentId,
            'external_id' => $rutRaw ?: $governmentId,
            'mail' => $email,
            'name' => $name,
            'send_mail' => false,
        ];

        if ($phone) {
            $payload['phone_number'] = $this->normalizePhone($phone);
        }

        Log::info("[TokuInvoice] Creando customer en Toku", ['payload' => $payload]);

        $response = $this->api->createCustomer($payload);

        $customer = TokuCustomer::updateOrCreate(
            ['partner_id' => $appointment->partner_id],
            [
                'code_toku' => $response['id'],
                'rut' => $rutRaw,
                'email' => $email,
                'name' => $name,
            ]
        );

        Log::info("[TokuInvoice] Customer creado", [
            'partner_id' => $customer->partner_id,
            'code_toku' => $customer->code_toku,
        ]);

        return $customer;
    }

    private function getOrCreateSubscription(TokuCustomer $customer, Appointment $appointment): string
    {
        if (!empty($customer->code_toku_subscription)) {
            return $customer->code_toku_subscription;
        }

        $rutLimpio = $customer->rut ? $this->cleanRut($customer->rut) : (string) $customer->partner_id;
        $productId = 'Agenda cita-' . $rutLimpio;

        $payload = [
            'product_id' => $productId,
            'customer' => $customer->code_toku,
            'metadata' => [
                'partner_id' => (int) $customer->partner_id,
                'description' => 'Citas Bradford - Apoderado ' . ($customer->name ?: $customer->partner_id),
            ],
        ];

        Log::info("[TokuInvoice] Creando subscription en Toku", ['payload' => $payload]);

        $response = $this->api->createSubscription($payload);

        $customer->code_toku_subscription = $response['id'];
        $customer->save();

        Log::info("[TokuInvoice] Subscription creada", [
            'partner_id' => $customer->partner_id,
            'subscription_id' => $customer->code_toku_subscription,
        ]);

        return $customer->code_toku_subscription;
    }

    private function createInvoice(TokuCustomer $customer, string $subscriptionId, Appointment $appointment, int $amount): array
    {
        $payload = [
            'subscription' => $subscriptionId,
            'customer' => $customer->code_toku,
            'invoice_external_id' => $appointment->code,
            'amount' => $amount,
            'due_date' => now()->addDays(7)->format('Y-m-d'),
            'currency_code' => 'CLP',
        ];

        Log::info("[TokuInvoice] Creando invoice en Toku", [
            'appointment_id' => $appointment->id,
            'code' => $appointment->code,
            'payload' => $payload,
        ]);

        return $this->api->createInvoice($payload);
    }

    public function buildPortalUrl(string $customerId, string $invoiceId): string
    {
        $baseUrl = rtrim(env('TOKU_PAYMENT_URL', ''), '/');
        $accountCode = env('TOKU_ACCOUNT_KEY', '');
        $invoices = json_encode([$invoiceId]);

        return "{$baseUrl}/onetime?accessSource=priv0&user={$customerId}&account={$accountCode}&invoices={$invoices}";
    }

    /**
     * Para government_id: solo quita puntos y guion, conserva el digito verificador.
     * Ej: "12.098.331-8" -> "120983318"
     */
    private function formatGovernmentId(string $rut): string
    {
        return strtolower(str_replace(['.', '-'], '', $rut));
    }

    /**
     * Para product_id de subscription: quita puntos y el digito verificador.
     * Ej: "12.098.331-8" -> "12098331"
     */
    private function cleanRut(string $rut): string
    {
        $clean = str_replace('.', '', $rut);
        if (strpos($clean, '-') !== false) {
            $clean = substr($clean, 0, strrpos($clean, '-'));
        }
        return strtolower($clean);
    }

    private function normalizePhone(string $phone): string
    {
        $clean = preg_replace('/[^0-9]/', '', $phone);
        if (strpos($clean, '56') !== 0) {
            $clean = '56' . $clean;
        }
        return '+' . $clean;
    }
}