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/api_matriculas/app/Services/Signapis/SignapisService.php
<?php

namespace App\Services\Signapis;

use App\Exceptions\SignapisException;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class SignapisService
{
    protected string $baseUrl;
    protected string $apiKey;
    protected string $token;
    protected string $emailAccount;
    protected string $passwordAccount;

    public function __construct(string $token = '', string $emailAccount = '', string $passwordAccount = '')
    {

        $firmaki = getConfiguration('firmaki');
        if (!$firmaki['status']) {
            throw new Exception("Integración está deshabilitada", 400);
        }
        $this->baseUrl = $firmaki['environment'] !=  'integration' ? $firmaki['url_production'] : (!empty($firmaki['url_sandbox']) ? $firmaki['url_sandbox'] : 'https://api-sandbox.signapis.com');
        if (empty($this->baseUrl)) {
            throw new Exception("URL no configurada", 400);
        }
        $this->versionApi = "/v2";
        // $this->apiKey  = 'cOIxw81DfI5BVMI5K5fAq1PPEj59aImbafEeikAz';
        $this->apiKey = $firmaki['api_key'];
        if (empty($this->apiKey)) {
            throw new Exception("APIKEY no configurada", 400);
        }
        $this->token   = ''; // el token de sesión se inyecta aquí
        // $this->emailAccount   = 'marcos@mdmn.cl'; // el email de la cuenta se inyecta aquí
        // $this->passwordAccount   = 'Lacalera2025*'; // el password de la cuenta se inyecta aquí
        $this->emailAccount =  $firmaki['email_account'];
        $this->passwordAccount =  $firmaki['password_account'];
        if (empty($this->emailAccount) || empty($this->passwordAccount)) {
            throw new Exception("Credenciales no configuradas", 400);
        }
    }


    private function authenticate(): void
    {
        $response = $this->validateAndCall(
            [
                'email' => $this->emailAccount,
                'password' => $this->passwordAccount,
            ],
            [
                'email'    => 'required|email',
                'password' => 'required|string|min:8',
            ],
            '/login',
            'POST',
            true // indica que es login
        );
         if (!empty($response['data']['accessToken'])) {
            Log::info(['response_OK'=>$response]);
            $this->token = $response['data']['accessToken'];
        } else {
            Log::info(['response_ERROR'=>$response]);
            throw new SignapisException("No se pudo obtener el token de autenticación", 401);
        }
    }

    private function call(string $endpoint, string $method = 'POST', array $options = [], $isLogin = false): array
    {

        // Si no es login → siempre hacemos login primero
        if (!$isLogin) {
            $login = $this->authenticate();
        }

        $headers = [
            'Content-Type' => 'application/json',
            'Accept'       => 'application/json',
        ];

        $headers['x-api-key'] = $this->apiKey;

        if ($this->token) {
            $headers['Authorization'] = "Bearer {$this->token}";
        }

        $response = Http::withHeaders($headers)
            ->timeout(30)
            ->$method("{$this->baseUrl}{$this->versionApi}{$endpoint}", $options);


        if (!$isLogin) {
            if ($response->failed()) {
                $errorResponse = json_decode($response->body(), true);

                $error = $errorResponse['message'] ?? $response->body();
                throw new \Exception("Error en petición a Signapis: $error", $response->status());
            }
        }


        return $response->json();
    }


    protected function validateAndCall(array $payload, array $rules, string $endpoint, string $method = 'POST', $isLogin = false): array
    {
        $validator = Validator::make($payload, $rules);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }
        return $this->call($endpoint, $method, $validator->validated(), $isLogin);
    }

    // ======================================
    // 🔹 AUTH
    // ======================================
    public function login(string $email = '', string $password = ''): array
    {
        $email = 'marcos@mdmn.cl';
        $password = 'Lacalera2025*';
        return $this->validateAndCall(
            compact('email', 'password'),
            [
                'email'    => 'required|email',
                'password' => 'required|string|min:8',
            ],
            '/login',
            'POST',
            true
        );
    }

    // ======================================
    // 🔹 WEBHOOKS
    // ======================================
    // 🔹 Obtener configuración actual del webhook de la compañía.
    public function getWebhook(): array
    {
        return $this->call('/company/webhook', 'get');
    }

    // 🔹 Configurar un webhook → suscribir eventos y definir la URL de notificación.
    public function configureWebhook(array $payload): array
    {
        // reglas según documentación oficial
        $rules = [
            'webhook'  => 'required|url',
            'authType' => 'required|string|in:WEBHOOK_NO_AUTH,WEBHOOK_BASIC_AUTH,WEBHOOK_OAUTH_TOKEN,WEBHOOK_API_KEY',
            'authValue' => 'required',
        ];

        return $this->validateAndCall($payload, $rules, '/company/webhook', 'post');
    }
    // 🔹 Eliminar el webhook actual de la compañía
    public function deleteWebhook(): array
    {
        return $this->call('/company/webhook', 'delete');
    }

    // 🔹 Probar un webhook enviando un evento de prueba.
    public function testWebhook(array $payload = []): array
    {
        return $this->call('/company/webhook/test', 'post', $payload);
    }

    // ======================================
    // 🔹 LOGOS
    // ======================================

    // 🔹 Crear un logo personalizado para usar en plantillas de correo.
    public function createLogo(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'name'  => 'required|string',
            'code'  => 'required|string|regex:/^[a-zA-Z0-9]+$/',
            'image' => 'required|string',
        ], '/company/emailLogos', 'post');
    }

    // 🔹 Listar todos los logos disponibles en la compañía.
    public function listLogos(): array
    {
        return $this->call('/company/emailLogos', 'get');
    }

    // 🔹 Obtener información de un logo específico mediante su código.
    public function getLogo(string $code): array
    {
        return $this->call("/company/emailLogos/{$code}", 'get');
    }

    // 🔹 Eliminar un logo específico de la compañía.
    public function deleteLogo(string $code): array
    {
        return $this->call("/company/emailLogos/{$code}", 'delete');
    }

    // ======================================
    // 🔹 TEMPLATES
    // ======================================
    // 🔹  Crear una plantilla de correo electrónico.
    public function createTemplate(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'name'    => 'required|string',
            'code'    => 'required|string',
            'subject' => 'required|string',
            'body'    => 'required|string',
        ], '/company/emailTemplates', 'post');
    }

    // 🔹 Listar todas las plantillas de correo disponibles.
    public function listTemplates(): array
    {
        return $this->call('/company/emailTemplates', 'get');
    }

    // 🔹 Obtener información de una plantilla específica.
    public function getTemplate(string $code, string $type): array
    {
        return $this->call("/company/emailTemplates/{$code}/{$type}", 'get');
    }

    // 🔹Modificar los datos de una plantilla existente. 
    public function updateTemplate(string $code, string $type, array $payload): array
    {
        return $this->call("/company/emailTemplates/{$code}/{$type}", 'patch', $payload);
    }

    // 🔹 Eliminar una plantilla específica.
    public function deleteTemplate(string $code, string $type): array
    {
        return $this->call("/company/emailTemplates/{$code}/{$type}", 'delete');
    }

    // 🔹 Eliminar todas las plantillas asociadas a un código.
    public function deleteTemplatesByCode(string $code): array
    {
        return $this->call("/company/emailTemplates/{$code}", 'delete');
    }

    // 🔹  Enviar un correo de prueba usando una plantilla.
    public function testTemplate(array $payload): array
    {
        return $this->call('/company/emailTemplates/test', 'post', $payload);
    }


    // ======================================
    // 🔹 NOTIFICATIONS
    // ======================================
    // 🔹 Reenviar invitación de firma a un firmante específico (correo/whatsapp).
    public function resendInvitation(string $signatoryId): array
    {
        return $this->call("/documents/signatories/{$signatoryId}/notify", 'post');
    }

    // ======================================
    // 🔹 DOCUMENTOS
    // ======================================

    // 🔹 Crear un documento para firma.
    public function createDocument(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'name'        => 'required|string',
            'document'    => 'required|string',
            'flow'        => 'required|string|in:INDIVIDUAL,SEQUENTIAL,PARALLEL',
            'type'        => 'nullable|string|in:ELECTRONIC',
            'observation' => 'nullable|string',
            'signatories' => 'required|array|min:1',
            'metadata'    => 'nullable|array',
        ], '/documents', 'post');
    }

    // 🔹 Obtener información de un documento por su documentId.
    public function getDocument(string $documentId): array
    {
        return $this->call("/documents/{$documentId}", 'get');
    }

    // 🔹 Agregar firmantes adicionales a un documento pendiente.
    public function addSignatories(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'documentId'  => 'required|string',
            'signatories' => 'required|array|min:1',
        ], '/documents/signatories', 'post');
    }

    // 🔹 Actualizar la información de un firmante de un documento.
    public function updateSignatory(string $signatoryId, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'email'        => 'nullable|email',
            'fullName'     => 'nullable|string',
            'phoneNumber'  => 'nullable|string|regex:/^\+\d{8,13}$/',
            'notification' => 'nullable|array',
            'positions'    => 'nullable|array',
        ], "/documents/signatories/{$signatoryId}", 'patch');
    }

    // 🔹 Eliminar un firmante de un documento pendiente.
    public function deleteSignatory(string $signatoryId): array
    {
        return $this->call("/documents/signatories/{$signatoryId}", 'delete');
    }

    // 🔹 Rechazar un documento como firmante.
    public function rejectDocument(string $signatoryId, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'observation'          => 'required|string|min:2',
            'geolocation'          => 'nullable|string|max:50',
            'geolocationReference' => 'nullable|string|max:300',
            'civilRegistry'        => 'nullable|array',
        ], "/documents/signatories/{$signatoryId}/reject", 'post');
    }

    // 🔹 Cancelar un documento usando el documentId.
    public function cancelDocumentById(string $documentId): array
    {
        return $this->call("/documents/{$documentId}/cancel", 'patch');
    }

    // 🔹 Cancelar un documento usando el documentClientId definido por el cliente.
    public function cancelDocumentByClientId(string $documentClientId): array
    {
        return $this->call("/documents/client/{$documentClientId}/cancel", 'patch');
    }

    // ======================================
    // 🔹 BATCH (Lotes de documentos)
    // ======================================

    // 🔹 Subir un documento temporal para incluirlo en un lote.
    public function uploadBatchFile(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'name'     => 'required|string',
            'document' => 'required|string',
        ], '/documents/batch/file', 'post');
    }

    // 🔹 Crear un lote de documentos para firma.
    public function createBatch(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'flow'        => 'required|string|in:INDIVIDUAL,SEQUENTIAL,PARALLEL',
            'documents'   => 'required|array|min:1',
            'signatories' => 'required|array|min:1',
            'metadata'    => 'nullable|array',
            'preSign'     => 'nullable|boolean',
        ], '/documents/batch', 'post');
    }

    // 🔹 Firmar un lote de documentos (signatario).
    public function signBatch(string $signatoryId, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'signatureType'        => 'required|integer|in:1,2,3',
            'signatureText'        => 'required_if:signatureType,1,3|string|max:120',
            'signatureImage'       => 'required_if:signatureType,2,3|string',
            'geolocation'          => 'nullable|string|max:50',
            'geolocationReference' => 'nullable|string|max:300',
            'civilRegistry'        => 'nullable|array',
        ], "/documents/batch/signatories/{$signatoryId}/sign", 'post');
    }

    // 🔹 Rechazar un lote de documentos como firmante.
    public function rejectBatch(string $signatoryId, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'observation'          => 'required|string|min:2',
            'geolocation'          => 'nullable|string|max:50',
            'geolocationReference' => 'nullable|string|max:300',
            'civilRegistry'        => 'nullable|array',
        ], "/documents/batch/signatories/{$signatoryId}/reject", 'post');
    }

    // 🔹 Obtener la información de un lote de documentos.
    public function getBatch(string $groupId): array
    {
        return $this->call("/documents/batch/{$groupId}", 'get');
    }

    // 🔹 Cancelar un lote de documentos por su groupId.
    public function cancelBatch(string $groupId): array
    {
        return $this->call("/documents/batch/{$groupId}", 'patch');
    }

    // 🔹 Certificar un lote previamente firmado (status PRESIGNED).
    public function certifyBatch(array $payload): array
    {
        return $this->validateAndCall($payload, [
            'groupId' => 'required|string',
        ], '/documents/batch/certify', 'post');
    }

    // 🔹 Obtener información del firmante de un lote firmado.
    public function getBatchSignerSummary(string $groupId, string $email): array
    {
        return $this->call("/documents/batch/summary/{$groupId}/{$email}", 'get');
    }

    // 🔹 Modificar datos de un firmante de un lote (estado PRESIGNED).
    public function updateBatchSigner(string $groupId, string $email, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'fullName'  => 'required|string',
            'email'     => 'required|email',
            'biometric' => 'required|array',
        ], "/documents/batch/summary/{$groupId}/{$email}", 'patch');
    }

    // 🔹 Firmar un documento individual (signatario).
    public function signDocument(string $signatoryId, array $payload): array
    {
        return $this->validateAndCall($payload, [
            'signatureType'        => 'required|integer|in:1,2,3',
            'signatureText'        => 'required_if:signatureType,1,3|string|max:120',
            'signatureImage'       => 'required_if:signatureType,2,3|string',
            'geolocation'          => 'nullable|string|max:50',
            'geolocationReference' => 'nullable|string|max:300',
            'civilRegistry'        => 'nullable|array',
        ], "/documents/signatories/{$signatoryId}/sign", 'post');
    }

    // ======================================
    // 🔹 REPORTES
    // ======================================

    // 🔹 Obtener listado de documentos filtrados por rango de fechas y estatus.
    public function getStatusDocumentsReport(string $start, string $end, string $status): array
    {
        $payload = compact('start', 'end', 'status');
        $this->validateAndCall($payload, [
            'start'  => 'required|date',
            'end'    => 'required|date',
            'status' => 'required|string|in:SIGNED,REJECTED,PENDING',
        ], '/dummy');

        $endpoint = "/reports/statusDocuments?start={$start}&end={$end}&status={$status}";
        return $this->call($endpoint, 'get');
    }

    // 🔹 Obtener resumen estadístico de documentos de la compañía entre fechas.
    public function getCompanyReport(string $start, string $end): array
    {
        $payload = compact('start', 'end');
        $this->validateAndCall($payload, [
            'start' => 'required|date',
            'end'   => 'required|date',
        ], '/dummy');

        $endpoint = "/reports/company?start={$start}&end={$end}";
        return $this->call($endpoint, 'get');
    }
}