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