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;
}
}