Actualiza frontend, integraciones y dist de producción
@@ -5,5 +5,11 @@ VITE_SUPABASE_URL=https://dbit.digitalcompass.agency
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_REEMPLAZAR
|
||||
# VITE_SUPABASE_ANON_KEY=REEMPLAZAR_CON_ANON_KEY
|
||||
|
||||
# Webhook de producción de n8n. El frontend lo notifica después de guardar cambios.
|
||||
# Webhook del flujo de recordatorios, conservado como referencia de producción.
|
||||
VITE_TAX_WEBHOOK_URL=https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos
|
||||
|
||||
# Busca empleados en BambooHR para completar contactos automáticamente.
|
||||
VITE_BAMBOOHR_CONTACT_WEBHOOK_URL=https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos-bamboohr-contacto
|
||||
|
||||
# Sincroniza altas, cambios y eliminaciones con Google Calendar.
|
||||
VITE_GOOGLE_CALENDAR_WEBHOOK_URL=https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos-google-calendar
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
- Nunca hardcodear correos autorizados en React: se administran desde Supabase.
|
||||
- Nunca exponer la llave `service_role`; el frontend sólo utiliza una llave pública o `anon`.
|
||||
- El workflow de n8n consulta contactos y obligaciones directamente desde Supabase.
|
||||
- Mantener la base de Vite en `/calendario-impuestos/` salvo cambio explícito de la ruta pública.
|
||||
- Mantener la base de Vite en `/` porque la app se publica en la raíz de `https://home.digitalcompass.agency/`.
|
||||
- Antes de entregar cambios, ejecutar `npm run typecheck`, `npm run lint` y `npm run build`.
|
||||
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Seguimiento de Impuestos GLM</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Calendario tributario GLM: recordatorios de obligaciones fiscales por país, con alertas del miércoles anterior y el mismo día."
|
||||
/>
|
||||
<meta name="author" content="GomezLee Marketing" />
|
||||
<meta name="theme-color" content="#4F758B" />
|
||||
<meta property="og:title" content="Seguimiento de Impuestos GLM" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Calendario tributario interno de GomezLee Marketing con recordatorios automáticos."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/calendario-impuestos/favicon-32.png?v=glm-2026" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/calendario-impuestos/favicon-192.png?v=glm-2026" />
|
||||
<link rel="shortcut icon" href="/calendario-impuestos/favicon.ico?v=glm-2026" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/calendario-impuestos/apple-touch-icon.png?v=glm-2026" />
|
||||
<script type="module" crossorigin src="/calendario-impuestos/assets/index-DjmlsRNV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/calendario-impuestos/assets/index-DrJGHNJG.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +1,96 @@
|
||||
# Configuración del flujo n8n
|
||||
# Configuración de n8n — Seguimiento de Impuestos GLM
|
||||
|
||||
Después de importar `Seguimiento-de-Impuestos-GLM.json`:
|
||||
Este proyecto incluye tres workflows independientes para facilitar pruebas y mantenimiento.
|
||||
|
||||
1. En el nodo **Gmail - Enviar recordatorio**, selecciona la credencial de Gmail que enviará los correos.
|
||||
2. Configura estas variables de entorno en n8n:
|
||||
## 1. Recordatorios por Gmail y Google Chat
|
||||
|
||||
```env
|
||||
SUPABASE_URL=https://dbit.digitalcompass.agency
|
||||
SUPABASE_SERVICE_ROLE_KEY=REEMPLAZAR
|
||||
WHATSAPP_BASE_URL=https://wsp.gomezleemarketing.com
|
||||
WHATSAPP_INSTANCE=botsoporte
|
||||
WHATSAPP_API_KEY=REEMPLAZAR_CON_LA_APIKEY
|
||||
GLM_TAX_APP_URL=https://digitalcompass.agency/calendario-impuestos/
|
||||
GLM_LOGO_URL=https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png
|
||||
```
|
||||
Archivo:
|
||||
|
||||
3. Activa el workflow. El Schedule Trigger está configurado para las 8:00 AM en `America/Santo_Domingo`.
|
||||
4. Para una prueba manual, usa **Ejecutar manualmente**. Para ejecutar una fecha específica por webhook:
|
||||
`Seguimiento-de-Impuestos-GLM-Recordatorios-Actualizado.json`
|
||||
|
||||
- Se ejecuta diariamente a las 8:00 a. m. en `America/Santo_Domingo`.
|
||||
- Consulta `tax_due_reminders` en Supabase.
|
||||
- Envía correos HTML con identidad visual GLM.
|
||||
- Envía mensajes por Google Chat.
|
||||
- Registra cada envío para evitar duplicados.
|
||||
|
||||
Después de importar:
|
||||
|
||||
1. Seleccionar la credencial correcta en `Gmail - Enviar recordatorio`.
|
||||
2. Confirmar la URL y API key del nodo de Google Chat.
|
||||
3. Desactivar el workflow anterior antes de activar este, para evitar duplicados.
|
||||
4. Ejecutar una prueba manual con una fecha controlada.
|
||||
|
||||
## 2. Búsqueda de contactos en BambooHR
|
||||
|
||||
Archivo:
|
||||
|
||||
`Seguimiento-de-Impuestos-GLM-BambooHR-Contactos.json`
|
||||
|
||||
Production URL esperada:
|
||||
|
||||
`https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos-bamboohr-contacto`
|
||||
|
||||
Después de importar:
|
||||
|
||||
1. Abrir `Generar reporte completo BambooHR`.
|
||||
2. Confirmar la credencial `BambooHR GLM Full Access`.
|
||||
3. Activar el workflow.
|
||||
4. Probar con:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "run_now",
|
||||
"run_date": "2026-07-24"
|
||||
"action": "search_contact",
|
||||
"full_name": "Nombre Apellido"
|
||||
}
|
||||
```
|
||||
|
||||
Las notificaciones del frontend (`contact.created`, `obligation.created`, etc.) sólo confirman cambios y no disparan recordatorios.
|
||||
El workflow normaliza nombres, tolera tildes y diferencias menores, prioriza coincidencias exactas y devuelve hasta ocho candidatos con país, teléfono, área, cargo, correo y estado.
|
||||
|
||||
## Enrutamiento regional por categoría
|
||||
## 3. Sincronización con Google Calendar
|
||||
|
||||
El workflow no requiere cambios para los nuevos contactos de Nómina. La RPC `tax_due_reminders` devuelve automáticamente:
|
||||
Archivo:
|
||||
|
||||
- contactos con área `regional`: todas las obligaciones;
|
||||
- contactos del país Regional con área `nomina`: Nómina de todos los países;
|
||||
- contactos del país Regional con área `admin`: Administración de todos los países;
|
||||
- contactos de un país específico: únicamente la categoría de ese país.
|
||||
`Seguimiento-de-Impuestos-GLM-Google-Calendar.json`
|
||||
|
||||
Production URL esperada:
|
||||
|
||||
`https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos-google-calendar`
|
||||
|
||||
Antes de activarlo, ejecutar en Supabase:
|
||||
|
||||
`../supabase/actualizacion_bamboohr_google_calendar.sql`
|
||||
|
||||
Después de importar:
|
||||
|
||||
1. Crear o seleccionar una credencial OAuth2 de Google Calendar.
|
||||
2. Asignarla en estos nodos:
|
||||
- `Google Calendar - Crear evento`
|
||||
- `Google Calendar - Actualizar evento`
|
||||
- `Google Calendar - Eliminar evento`
|
||||
3. Verificar que la cuenta conectada sea la cuenta organizadora de los eventos.
|
||||
4. Activar el workflow.
|
||||
|
||||
Cada obligación crea un evento de 8:00 a. m. a 9:00 a. m., hora de República Dominicana, e invita a los contactos aplicables. Las modificaciones y eliminaciones realizadas antes del primer aviso también se sincronizan.
|
||||
|
||||
## Orden recomendado de despliegue
|
||||
|
||||
1. Ejecutar el parche SQL.
|
||||
2. Importar y activar BambooHR.
|
||||
3. Importar y activar Google Calendar.
|
||||
4. Reemplazar el workflow de recordatorios.
|
||||
5. Desplegar el frontend actualizado.
|
||||
6. Probar un contacto y una obligación futura de prueba.
|
||||
|
||||
|
||||
## Google Chat — mensajes directos
|
||||
|
||||
El workflow de recordatorios usa una credencial **Google Chat OAuth2 API** con los scopes incluidos por n8n (`chat.spaces`, `chat.messages` y `chat.memberships`). Después de importar:
|
||||
|
||||
1. Habilita Google Chat API en el proyecto de Google Cloud.
|
||||
2. Crea o selecciona una credencial `Google Chat OAuth2 API` en n8n.
|
||||
3. Asígnala a `Google Chat - Abrir DM` y `Google Chat - Enviar recordatorio`.
|
||||
4. Autentica con la cuenta corporativa que enviará los mensajes.
|
||||
5. Ejecuta una prueba manual antes de activar el Schedule Trigger.
|
||||
|
||||
El flujo crea o reutiliza un mensaje directo con el correo corporativo del contacto, envía el aviso y registra el resultado en `tax_notification_log` con canal `google_chat`.
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
{
|
||||
"name": "Seguimiento de Impuestos GLM - Google Calendar",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "seguimiento-impuestos-google-calendar",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
320,
|
||||
480
|
||||
],
|
||||
"id": "1969add0-835c-459f-906e-ecb0bc43b175",
|
||||
"name": "Webhook - Google Calendar",
|
||||
"webhookId": "seguimiento-impuestos-google-calendar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "let body = $json.body ?? $json;\nif (typeof body === 'string') {\n try { body = JSON.parse(body); }\n catch { body = {}; }\n}\nconst action = String(body.action ?? '').trim().toLowerCase();\nconst payload = body.payload ?? {};\nconst obligationId = String(payload.obligation_id ?? body.obligation_id ?? '').trim();\nconst allowed = ['obligation.created', 'obligation.updated', 'obligation.deleted'];\nreturn [{\n json: {\n valid: allowed.includes(action) && Boolean(obligationId),\n action,\n obligation_id: obligationId,\n message: allowed.includes(action)\n ? (obligationId ? '' : 'Falta obligation_id.')\n : 'Acción no soportada.',\n },\n}];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
560,
|
||||
480
|
||||
],
|
||||
"id": "4257a973-d67c-4972-a140-0acdeaa0367e",
|
||||
"name": "Preparar solicitud"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "771f008a-734d-436d-9122-448758a2263a",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
800,
|
||||
480
|
||||
],
|
||||
"id": "7714ac21-6eed-4eaa-aa57-1f145ace0b13",
|
||||
"name": "Solicitud válida"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_google_calendar_payload",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { p_obligation_id: $json.obligation_id } }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1056,
|
||||
384
|
||||
],
|
||||
"id": "60720896-3eef-4a6f-9c8f-406bc777137b",
|
||||
"name": "Supabase - Obtener obligación y asistentes"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const source = $('Preparar solicitud').item.json;\nconst value = $json;\nconst row = Array.isArray(value) ? value[0] : Array.isArray(value?.data) ? value.data[0] : value;\nif (!row?.obligation_id) {\n return [{ json: { ...source, operation: 'noop', reason: 'No se encontró la obligación.' } }];\n}\n\nconst attendees = Array.isArray(row.attendees)\n ? [...new Set(row.attendees.map((email) => String(email ?? '').trim().toLowerCase()).filter(Boolean))]\n : [];\nconst start = `${row.due_date}T08:00:00-04:00`;\nconst end = `${row.due_date}T09:00:00-04:00`;\nconst categoryLabel = row.category === 'nomina' ? 'Nómina' : 'Administración';\nconst summary = `Impuestos · ${row.country_name} · ${row.description}`;\nconst description = [\n 'Seguimiento de Impuestos GLM',\n '',\n `País: ${row.country_name}`,\n `Categoría: ${categoryLabel}`,\n `Fecha límite: ${row.due_date}`,\n `Obligación: ${row.description}`,\n '',\n 'Abrir aplicación: https://home.digitalcompass.agency/',\n].join('\\n');\nconst calendarBody = {\n summary,\n description,\n start: { dateTime: start, timeZone: 'America/Santo_Domingo' },\n end: { dateTime: end, timeZone: 'America/Santo_Domingo' },\n attendees: attendees.map((email) => ({ email })),\n reminders: {\n useDefault: false,\n overrides: [\n { method: 'email', minutes: 0 },\n { method: 'popup', minutes: 0 },\n ],\n },\n extendedProperties: {\n private: {\n glmTaxObligationId: row.obligation_id,\n glmTaxCountryCode: row.country_code,\n glmTaxCategory: row.category,\n },\n },\n};\n\nlet operation = 'create';\nif (source.action === 'obligation.deleted' || row.active === false) {\n operation = row.google_event_id ? 'delete' : 'noop';\n} else if (row.google_event_id) {\n operation = 'update';\n}\n\nreturn [{\n json: {\n ...source,\n ...row,\n attendees,\n start,\n end,\n summary,\n calendar_description: description,\n calendar_body: calendarBody,\n calendar_id: row.calendar_id || 'primary',\n operation,\n },\n}];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1312,
|
||||
384
|
||||
],
|
||||
"id": "437c0861-d237-43a9-acb6-a3cecad1c748",
|
||||
"name": "Preparar payload Calendar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().filter((item) => item.json.operation === 'create');"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1568,
|
||||
256
|
||||
],
|
||||
"id": "21075755-7ae2-4f5a-ae0e-63e5e62b9e78",
|
||||
"name": "Preparar crear"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().filter((item) => item.json.operation === 'update');"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1568,
|
||||
416
|
||||
],
|
||||
"id": "b20f873b-1b6d-4fde-8832-1dae87674f60",
|
||||
"name": "Preparar actualizar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().filter((item) => item.json.operation === 'delete');"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1568,
|
||||
576
|
||||
],
|
||||
"id": "21a0f3e2-c6e6-40e8-b013-30ba4c739a44",
|
||||
"name": "Preparar eliminar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().filter((item) => item.json.operation === 'noop');"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1568,
|
||||
736
|
||||
],
|
||||
"id": "9efc968a-6ba7-41bd-8bdb-f391a6b482f2",
|
||||
"name": "Preparar sin acción"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "googleCalendarOAuth2Api",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ $json.calendar_body }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1824,
|
||||
256
|
||||
],
|
||||
"id": "b4a40589-9002-4479-8576-7c01d746fb65",
|
||||
"name": "Google Calendar - Crear evento",
|
||||
"credentials": {
|
||||
"googleCalendarOAuth2Api": {
|
||||
"id": "REEMPLAZAR_CREDENCIAL_GOOGLE_CALENDAR",
|
||||
"name": "Google Calendar GLM"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/primary/events/' + encodeURIComponent($json.google_event_id) + '?sendUpdates=all' }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "googleCalendarOAuth2Api",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ $json.calendar_body }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1824,
|
||||
416
|
||||
],
|
||||
"id": "443971f0-bb68-4051-adbc-81f46e7cf785",
|
||||
"name": "Google Calendar - Actualizar evento",
|
||||
"credentials": {
|
||||
"googleCalendarOAuth2Api": {
|
||||
"id": "REEMPLAZAR_CREDENCIAL_GOOGLE_CALENDAR",
|
||||
"name": "Google Calendar GLM"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "DELETE",
|
||||
"url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/primary/events/' + encodeURIComponent($json.google_event_id) + '?sendUpdates=all' }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "googleCalendarOAuth2Api",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1824,
|
||||
576
|
||||
],
|
||||
"id": "9658c53c-ebeb-4c28-94cd-e56011a6175e",
|
||||
"name": "Google Calendar - Eliminar evento",
|
||||
"credentials": {
|
||||
"googleCalendarOAuth2Api": {
|
||||
"id": "REEMPLAZAR_CREDENCIAL_GOOGLE_CALENDAR",
|
||||
"name": "Google Calendar GLM"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const source = $('Preparar crear').item.json; return [{ json: { ...source, google_event_id: String($json.id ?? ''), google_response: $json, sync_action: 'create', sync_status: 'active' } }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2080,
|
||||
256
|
||||
],
|
||||
"id": "a807b37a-f61c-46de-87f8-74a436a92e00",
|
||||
"name": "Preparar registro creado"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const source = $('Preparar actualizar').item.json; return [{ json: { ...source, google_event_id: source.google_event_id, google_response: $json, sync_action: 'update', sync_status: 'active' } }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2080,
|
||||
416
|
||||
],
|
||||
"id": "497396c0-d40e-4613-aeb4-baf52ce8f447",
|
||||
"name": "Preparar registro actualizado"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const source = $('Preparar eliminar').item.json; return [{ json: { ...source, google_event_id: source.google_event_id, google_response: $json ?? {}, sync_action: 'delete', sync_status: 'deleted' } }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2080,
|
||||
576
|
||||
],
|
||||
"id": "034ad2a5-526e-4d4c-9a56-db9e83bde071",
|
||||
"name": "Preparar registro eliminado"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { p_obligation_id: $('Preparar registro creado').item.json.obligation_id, p_google_event_id: $('Preparar registro creado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro creado').item.json.attendees, p_status: $('Preparar registro creado').item.json.sync_status, p_last_action: $('Preparar registro creado').item.json.sync_action, p_provider_response: $('Preparar registro creado').item.json.google_response } }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
2336,
|
||||
256
|
||||
],
|
||||
"id": "5072ebec-03ee-49e7-b050-6cc93b538f7b",
|
||||
"name": "Supabase - Registrar creación Calendar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { p_obligation_id: $('Preparar registro actualizado').item.json.obligation_id, p_google_event_id: $('Preparar registro actualizado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro actualizado').item.json.attendees, p_status: $('Preparar registro actualizado').item.json.sync_status, p_last_action: $('Preparar registro actualizado').item.json.sync_action, p_provider_response: $('Preparar registro actualizado').item.json.google_response } }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
2336,
|
||||
416
|
||||
],
|
||||
"id": "774d2dab-bb85-44e5-aeaf-e4dfe6954931",
|
||||
"name": "Supabase - Registrar actualización Calendar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/tax_record_google_calendar_sync",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apikey",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
|
||||
},
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { p_obligation_id: $('Preparar registro eliminado').item.json.obligation_id, p_google_event_id: $('Preparar registro eliminado').item.json.google_event_id, p_calendar_id: 'primary', p_attendees: $('Preparar registro eliminado').item.json.attendees, p_status: $('Preparar registro eliminado').item.json.sync_status, p_last_action: $('Preparar registro eliminado').item.json.sync_action, p_provider_response: $('Preparar registro eliminado').item.json.google_response } }}",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
2336,
|
||||
576
|
||||
],
|
||||
"id": "67986a91-dbb4-4ab2-b33e-e003ac8895df",
|
||||
"name": "Supabase - Registrar eliminación Calendar"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { ok: true, message: 'Evento creado en Google Calendar.' } }}",
|
||||
"options": {
|
||||
"responseCode": 200,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"name": "Cache-Control",
|
||||
"value": "no-store"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
2592,
|
||||
256
|
||||
],
|
||||
"id": "932c9da0-2a24-4eb3-ac40-3ccaf5b1895a",
|
||||
"name": "Responder creación"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { ok: true, message: 'Evento actualizado en Google Calendar.' } }}",
|
||||
"options": {
|
||||
"responseCode": 200,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"name": "Cache-Control",
|
||||
"value": "no-store"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
2592,
|
||||
416
|
||||
],
|
||||
"id": "bbeb54b6-30c9-43f8-9c54-7564a1684d3b",
|
||||
"name": "Responder actualización"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { ok: true, message: 'Evento eliminado de Google Calendar.' } }}",
|
||||
"options": {
|
||||
"responseCode": 200,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"name": "Cache-Control",
|
||||
"value": "no-store"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
2592,
|
||||
576
|
||||
],
|
||||
"id": "51bee51d-e61a-473a-a200-a447b295a59e",
|
||||
"name": "Responder eliminación"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { ok: true, message: $json.reason || 'No había cambios que sincronizar.' } }}",
|
||||
"options": {
|
||||
"responseCode": 200,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
1824,
|
||||
736
|
||||
],
|
||||
"id": "f3b38547-3de2-4499-94c8-7a331cae5c9a",
|
||||
"name": "Responder sin acción"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { ok: false, message: $json.message } }}",
|
||||
"options": {
|
||||
"responseCode": 400,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.4,
|
||||
"position": [
|
||||
1056,
|
||||
656
|
||||
],
|
||||
"id": "558b4170-3ae9-471d-a506-5d404ccde75e",
|
||||
"name": "Responder solicitud inválida"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook - Google Calendar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar solicitud",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar solicitud": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Solicitud válida",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Solicitud válida": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Supabase - Obtener obligación y asistentes",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Responder solicitud inválida",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Supabase - Obtener obligación y asistentes": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar payload Calendar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar payload Calendar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar crear",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Preparar actualizar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Preparar eliminar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Preparar sin acción",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar crear": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Google Calendar - Crear evento",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Google Calendar - Crear evento": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar registro creado",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar registro creado": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Supabase - Registrar creación Calendar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Supabase - Registrar creación Calendar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Responder creación",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar actualizar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Google Calendar - Actualizar evento",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Google Calendar - Actualizar evento": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar registro actualizado",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar registro actualizado": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Supabase - Registrar actualización Calendar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Supabase - Registrar actualización Calendar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Responder actualización",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar eliminar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Google Calendar - Eliminar evento",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Google Calendar - Eliminar evento": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Preparar registro eliminado",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar registro eliminado": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Supabase - Registrar eliminación Calendar",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Supabase - Registrar eliminación Calendar": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Responder eliminación",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Preparar sin acción": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Responder sin acción",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "America/Santo_Domingo",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -1,5 +1,12 @@
|
||||
import type { SupabaseAuthSession } from "./supabase-auth";
|
||||
import type { Contact, ContactArea, Country, TaxCategory, TaxEvent } from "./tax-utils";
|
||||
import type {
|
||||
BambooContactCandidate,
|
||||
Contact,
|
||||
ContactArea,
|
||||
Country,
|
||||
TaxCategory,
|
||||
TaxEvent,
|
||||
} from "./tax-utils";
|
||||
|
||||
const supabaseUrl = String(import.meta.env.VITE_SUPABASE_URL ?? "")
|
||||
.trim()
|
||||
@@ -7,7 +14,12 @@ const supabaseUrl = String(import.meta.env.VITE_SUPABASE_URL ?? "")
|
||||
const publishableKey = String(import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY ?? "").trim();
|
||||
const anonKey = String(import.meta.env.VITE_SUPABASE_ANON_KEY ?? "").trim();
|
||||
const supabaseKey = publishableKey || anonKey;
|
||||
const workflowUrl = String(import.meta.env.VITE_TAX_WEBHOOK_URL ?? "").trim();
|
||||
const bambooContactWorkflowUrl = String(
|
||||
import.meta.env.VITE_BAMBOOHR_CONTACT_WEBHOOK_URL ?? "",
|
||||
).trim();
|
||||
const googleCalendarWorkflowUrl = String(
|
||||
import.meta.env.VITE_GOOGLE_CALENDAR_WEBHOOK_URL ?? "",
|
||||
).trim();
|
||||
|
||||
type CountryRow = {
|
||||
id: string;
|
||||
@@ -45,11 +57,18 @@ type ContactRow = {
|
||||
country_id: string;
|
||||
full_name: string;
|
||||
email: string | null;
|
||||
whatsapp_number: string | null;
|
||||
job_title: string | null;
|
||||
area: ContactArea;
|
||||
department: string | null;
|
||||
division: string | null;
|
||||
location: string | null;
|
||||
source: string | null;
|
||||
bamboo_employee_id: string | null;
|
||||
bamboo_employee_number: string | null;
|
||||
bamboo_status: string | null;
|
||||
email_enabled: boolean;
|
||||
whatsapp_enabled: boolean;
|
||||
google_chat_enabled: boolean;
|
||||
calendar_enabled: boolean;
|
||||
};
|
||||
|
||||
export type NewObligation = {
|
||||
@@ -63,11 +82,24 @@ export type NewContact = {
|
||||
countryId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
area: ContactArea;
|
||||
department?: string;
|
||||
division?: string;
|
||||
location?: string;
|
||||
source?: "manual" | "bamboohr" | string;
|
||||
bambooEmployeeId?: string;
|
||||
bambooEmployeeNumber?: string;
|
||||
bambooStatus?: string;
|
||||
emailEnabled: boolean;
|
||||
whatsappEnabled: boolean;
|
||||
googleChatEnabled: boolean;
|
||||
calendarEnabled: boolean;
|
||||
};
|
||||
|
||||
type BambooSearchResponse = {
|
||||
ok?: boolean;
|
||||
message?: string;
|
||||
candidates?: BambooContactCandidate[];
|
||||
};
|
||||
|
||||
export async function loadObligationDeliveryStatuses(
|
||||
@@ -102,7 +134,7 @@ export async function loadTaxData(session: SupabaseAuthSession): Promise<{
|
||||
),
|
||||
request<ContactRow[]>(
|
||||
session,
|
||||
"/rest/v1/tax_contacts?select=id,country_id,full_name,email,whatsapp_number,job_title,area,email_enabled,whatsapp_enabled&active=eq.true&order=full_name.asc",
|
||||
"/rest/v1/tax_contacts?select=id,country_id,full_name,email,job_title,area,department,division,location,source,bamboo_employee_id,bamboo_employee_number,bamboo_status,email_enabled,google_chat_enabled,calendar_enabled&active=eq.true&order=full_name.asc",
|
||||
),
|
||||
loadObligationDeliveryStatuses(session),
|
||||
]);
|
||||
@@ -128,23 +160,7 @@ export async function loadTaxData(session: SupabaseAuthSession): Promise<{
|
||||
} satisfies TaxEvent;
|
||||
});
|
||||
|
||||
const contacts = contactRows.map((row) => {
|
||||
const country = countryMap.get(row.country_id);
|
||||
if (!country) throw new Error("Un contacto tiene un país inválido.");
|
||||
return {
|
||||
id: row.id,
|
||||
countryId: row.country_id,
|
||||
country: country.name,
|
||||
countryCode: country.code,
|
||||
name: row.full_name,
|
||||
email: row.email ?? "",
|
||||
phone: row.whatsapp_number ?? "",
|
||||
role: row.job_title ?? "",
|
||||
area: row.area,
|
||||
emailEnabled: row.email_enabled,
|
||||
whatsappEnabled: row.whatsapp_enabled,
|
||||
} satisfies Contact;
|
||||
});
|
||||
const contacts = contactRows.map((row) => mapContactRow(row, countryMap));
|
||||
|
||||
return { countries, events, contacts };
|
||||
}
|
||||
@@ -153,6 +169,8 @@ export async function createObligation(
|
||||
session: SupabaseAuthSession,
|
||||
input: NewObligation,
|
||||
): Promise<TaxEvent> {
|
||||
assertNotPastDate(input.date);
|
||||
|
||||
const [row] = await request<ObligationRow[]>(session, "/rest/v1/tax_obligations", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
@@ -170,10 +188,11 @@ export async function createObligation(
|
||||
const country = countries.find((item) => item.id === row.country_id);
|
||||
if (!country) throw new Error("No se encontró el país de la obligación.");
|
||||
|
||||
void notifyWorkflow("obligation.created", {
|
||||
const payload = {
|
||||
obligation_id: row.id,
|
||||
due_date: row.due_date,
|
||||
});
|
||||
};
|
||||
void notifyGoogleCalendar("obligation.created", payload);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -195,6 +214,8 @@ export async function updateObligation(
|
||||
id: string,
|
||||
input: NewObligation,
|
||||
): Promise<TaxEvent> {
|
||||
assertNotPastDate(input.date);
|
||||
|
||||
const [row] = await request<ObligationRow[]>(
|
||||
session,
|
||||
`/rest/v1/tax_obligations?id=eq.${encodeURIComponent(id)}`,
|
||||
@@ -216,10 +237,11 @@ export async function updateObligation(
|
||||
const country = countries.find((item) => item.id === row.country_id);
|
||||
if (!country) throw new Error("No se encontró el país de la obligación.");
|
||||
|
||||
void notifyWorkflow("obligation.updated", {
|
||||
const payload = {
|
||||
obligation_id: row.id,
|
||||
due_date: row.due_date,
|
||||
});
|
||||
};
|
||||
void notifyGoogleCalendar("obligation.updated", payload);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -242,7 +264,8 @@ export async function removeObligation(session: SupabaseAuthSession, id: string)
|
||||
headers: { Prefer: "return=minimal" },
|
||||
body: JSON.stringify({ active: false }),
|
||||
});
|
||||
void notifyWorkflow("obligation.deleted", { obligation_id: id });
|
||||
const payload = { obligation_id: id };
|
||||
void notifyGoogleCalendar("obligation.deleted", payload);
|
||||
}
|
||||
|
||||
export async function createContact(
|
||||
@@ -252,40 +275,38 @@ export async function createContact(
|
||||
const [row] = await request<ContactRow[]>(session, "/rest/v1/tax_contacts", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
country_id: input.countryId,
|
||||
full_name: input.name.trim(),
|
||||
email: input.email.trim() || null,
|
||||
whatsapp_number: normalizeWhatsApp(input.phone) || null,
|
||||
job_title: input.role.trim() || null,
|
||||
area: input.area,
|
||||
email_enabled: input.emailEnabled,
|
||||
whatsapp_enabled: input.whatsappEnabled,
|
||||
}),
|
||||
body: JSON.stringify(contactPayload(input)),
|
||||
});
|
||||
|
||||
const countries = await getCountries(session);
|
||||
const country = countries.find((item) => item.id === row.country_id);
|
||||
if (!country) throw new Error("No se encontró el país del contacto.");
|
||||
const countryMap = new Map(countries.map((country) => [country.id, country]));
|
||||
const created = mapContactRow(row, countryMap);
|
||||
|
||||
void notifyWorkflow("contact.created", {
|
||||
contact_id: row.id,
|
||||
country_id: row.country_id,
|
||||
});
|
||||
return created;
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
countryId: row.country_id,
|
||||
country: country.name,
|
||||
countryCode: country.code,
|
||||
name: row.full_name,
|
||||
email: row.email ?? "",
|
||||
phone: row.whatsapp_number ?? "",
|
||||
role: row.job_title ?? "",
|
||||
area: row.area,
|
||||
emailEnabled: row.email_enabled,
|
||||
whatsappEnabled: row.whatsapp_enabled,
|
||||
};
|
||||
export async function updateContact(
|
||||
session: SupabaseAuthSession,
|
||||
id: string,
|
||||
input: NewContact,
|
||||
): Promise<Contact> {
|
||||
const [row] = await request<ContactRow[]>(
|
||||
session,
|
||||
`/rest/v1/tax_contacts?id=eq.${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify(contactPayload(input)),
|
||||
},
|
||||
);
|
||||
|
||||
if (!row) throw new Error("No se encontró el contacto.");
|
||||
|
||||
const countries = await getCountries(session);
|
||||
const countryMap = new Map(countries.map((country) => [country.id, country]));
|
||||
const updated = mapContactRow(row, countryMap);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function removeContact(session: SupabaseAuthSession, id: string): Promise<void> {
|
||||
@@ -294,7 +315,50 @@ export async function removeContact(session: SupabaseAuthSession, id: string): P
|
||||
headers: { Prefer: "return=minimal" },
|
||||
body: JSON.stringify({ active: false }),
|
||||
});
|
||||
void notifyWorkflow("contact.deleted", { contact_id: id });
|
||||
}
|
||||
|
||||
export async function searchBambooContacts(
|
||||
session: SupabaseAuthSession,
|
||||
fullName: string,
|
||||
): Promise<BambooContactCandidate[]> {
|
||||
if (!bambooContactWorkflowUrl) {
|
||||
throw new Error("Falta configurar el webhook de BambooHR.");
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), 300_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(bambooContactWorkflowUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
||||
body: JSON.stringify({
|
||||
action: "search_contact",
|
||||
full_name: fullName.trim(),
|
||||
requested_by: session.user.email,
|
||||
source: "seguimiento-impuestos-glm",
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("No pudimos consultar BambooHR.");
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as BambooSearchResponse;
|
||||
if (payload.ok === false) {
|
||||
throw new Error(payload.message || "No pudimos consultar BambooHR.");
|
||||
}
|
||||
|
||||
return Array.isArray(payload.candidates) ? payload.candidates : [];
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw new Error("La búsqueda tardó demasiado. Inténtalo nuevamente.");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCountries(session: SupabaseAuthSession): Promise<Country[]> {
|
||||
@@ -335,6 +399,86 @@ async function request<T = unknown>(
|
||||
return (text ? JSON.parse(text) : undefined) as T;
|
||||
}
|
||||
|
||||
function mapContactRow(row: ContactRow, countryMap: Map<string, Country>): Contact {
|
||||
const country = countryMap.get(row.country_id);
|
||||
if (!country) throw new Error("Un contacto tiene un país inválido.");
|
||||
return {
|
||||
id: row.id,
|
||||
countryId: row.country_id,
|
||||
country: country.name,
|
||||
countryCode: country.code,
|
||||
name: row.full_name,
|
||||
email: row.email ?? "",
|
||||
role: row.job_title ?? "",
|
||||
area: row.area,
|
||||
department: row.department ?? "",
|
||||
division: row.division ?? "",
|
||||
location: row.location ?? "",
|
||||
source: row.source ?? "manual",
|
||||
bambooEmployeeId: row.bamboo_employee_id ?? "",
|
||||
bambooEmployeeNumber: row.bamboo_employee_number ?? "",
|
||||
bambooStatus: row.bamboo_status ?? "",
|
||||
emailEnabled: row.email_enabled,
|
||||
googleChatEnabled: row.google_chat_enabled,
|
||||
calendarEnabled: row.calendar_enabled,
|
||||
};
|
||||
}
|
||||
|
||||
function contactPayload(input: NewContact) {
|
||||
return {
|
||||
country_id: input.countryId,
|
||||
full_name: input.name.trim(),
|
||||
email: input.email.trim() || null,
|
||||
job_title: input.role.trim() || null,
|
||||
area: input.area,
|
||||
department: input.department?.trim() || null,
|
||||
division: input.division?.trim() || null,
|
||||
location: input.location?.trim() || null,
|
||||
source: input.source || "manual",
|
||||
bamboo_employee_id: input.bambooEmployeeId?.trim() || null,
|
||||
bamboo_employee_number: input.bambooEmployeeNumber?.trim() || null,
|
||||
bamboo_status: input.bambooStatus?.trim() || null,
|
||||
email_enabled: input.emailEnabled,
|
||||
google_chat_enabled: input.googleChatEnabled,
|
||||
calendar_enabled: input.calendarEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
function assertNotPastDate(value: string): void {
|
||||
const date = String(value ?? "").trim();
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||
|
||||
if (!match) {
|
||||
throw new Error("Selecciona una fecha válida.");
|
||||
}
|
||||
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
const parsed = new Date(Date.UTC(year, month - 1, day));
|
||||
|
||||
if (
|
||||
parsed.getUTCFullYear() !== year ||
|
||||
parsed.getUTCMonth() !== month - 1 ||
|
||||
parsed.getUTCDate() !== day
|
||||
) {
|
||||
throw new Error("Selecciona una fecha válida.");
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: "America/Santo_Domingo",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).formatToParts(new Date());
|
||||
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
||||
const today = `${values.year}-${values.month}-${values.day}`;
|
||||
|
||||
if (date < today) {
|
||||
throw new Error("No puedes crear ni mover una obligación a una fecha que ya pasó.");
|
||||
}
|
||||
}
|
||||
|
||||
async function readError(response: Response): Promise<string> {
|
||||
try {
|
||||
const payload = (await response.json()) as { message?: string; details?: string };
|
||||
@@ -344,23 +488,20 @@ async function readError(response: Response): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWhatsApp(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.includes("@g.us") || trimmed.includes("@s.whatsapp.net")) return trimmed;
|
||||
return trimmed.replace(/\D/g, "");
|
||||
async function notifyGoogleCalendar(action: string, payload: Record<string, unknown>) {
|
||||
await postWebhook(googleCalendarWorkflowUrl, action, payload);
|
||||
}
|
||||
|
||||
async function notifyWorkflow(action: string, payload: Record<string, unknown>) {
|
||||
if (!workflowUrl) return;
|
||||
async function postWebhook(url: string, action: string, payload: Record<string, unknown>) {
|
||||
if (!url) return;
|
||||
try {
|
||||
await fetch(workflowUrl, {
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
||||
body: JSON.stringify({ action, payload, source: "frontend" }),
|
||||
keepalive: true,
|
||||
});
|
||||
} catch {
|
||||
// La base de datos ya guardó el cambio. El webhook es sólo una notificación.
|
||||
// La base de datos ya guardó el cambio. El webhook es una sincronización secundaria.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export type TaxEvent = {
|
||||
sentNotificationCount: number;
|
||||
};
|
||||
|
||||
export type ContactArea = "admin" | "nomina" | "regional";
|
||||
export type ContactArea = "admin" | "nomina" | "regional" | "other";
|
||||
|
||||
export type Contact = {
|
||||
id: string;
|
||||
@@ -31,11 +31,36 @@ export type Contact = {
|
||||
countryCode: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
role: string;
|
||||
area: ContactArea;
|
||||
department: string;
|
||||
division: string;
|
||||
location: string;
|
||||
source: "manual" | "bamboohr" | string;
|
||||
bambooEmployeeId: string;
|
||||
bambooEmployeeNumber: string;
|
||||
bambooStatus: string;
|
||||
emailEnabled: boolean;
|
||||
whatsappEnabled: boolean;
|
||||
googleChatEnabled: boolean;
|
||||
calendarEnabled: boolean;
|
||||
};
|
||||
|
||||
export type BambooContactCandidate = {
|
||||
id: string;
|
||||
employeeNumber: string;
|
||||
fullName: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
jobTitle: string;
|
||||
department: string;
|
||||
division: string;
|
||||
location: string;
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
area: ContactArea;
|
||||
status: string;
|
||||
score: number;
|
||||
matchQuality: "exacta" | "alta" | "media";
|
||||
};
|
||||
|
||||
// Siempre se alerta el miércoles anterior (estrictamente anterior) y la fecha
|
||||
|
||||
@@ -5,6 +5,8 @@ interface ImportMetaEnv {
|
||||
readonly VITE_SUPABASE_PUBLISHABLE_KEY?: string;
|
||||
readonly VITE_SUPABASE_ANON_KEY?: string;
|
||||
readonly VITE_TAX_WEBHOOK_URL?: string;
|
||||
readonly VITE_BAMBOOHR_CONTACT_WEBHOOK_URL?: string;
|
||||
readonly VITE_GOOGLE_CALENDAR_WEBHOOK_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Configuración de Supabase
|
||||
|
||||
Ejecuta una sola vez el script completo:
|
||||
## Base existente
|
||||
|
||||
La base ya recibió `actualizacion_bamboohr_google_calendar.sql`. Ejecuta ahora una sola vez:
|
||||
|
||||
```text
|
||||
actualizacion_google_chat_sin_whatsapp.sql
|
||||
```
|
||||
|
||||
Este parche agrega Google Chat, desactiva WhatsApp sin borrar datos históricos y actualiza la RPC de recordatorios.
|
||||
|
||||
## Instalación nueva
|
||||
|
||||
Ejecuta:
|
||||
|
||||
```text
|
||||
Seguimiento-de-Impuestos-GLM.sql
|
||||
```
|
||||
|
||||
El script crea el control de acceso, países, obligaciones, contactos, bitácora de envíos, RLS, funciones para n8n, bloqueo de eventos notificados y los datos históricos de 2026.
|
||||
|
||||
## Si la base ya estaba creada
|
||||
|
||||
Ejecuta solamente esta actualización:
|
||||
|
||||
```text
|
||||
actualizacion_eventos_bloqueados.sql
|
||||
```
|
||||
|
||||
Esta función permite que el frontend consulte si un evento ya generó avisos y agrega una protección en PostgreSQL para impedir su modificación o eliminación después del primer envío.
|
||||
|
||||
## Añadir o reactivar un usuario
|
||||
## Accesos
|
||||
|
||||
```sql
|
||||
insert into public.tax_calendar_access (email, full_name)
|
||||
@@ -28,23 +28,3 @@ set active = true,
|
||||
full_name = excluded.full_name,
|
||||
updated_at = now();
|
||||
```
|
||||
|
||||
## Desactivar un usuario
|
||||
|
||||
```sql
|
||||
update public.tax_calendar_access
|
||||
set active = false
|
||||
where email = 'persona@gomezleemarketing.com';
|
||||
```
|
||||
|
||||
La aplicación consulta la función segura `public.has_tax_calendar_access()`. La tabla de accesos no se expone directamente al navegador.
|
||||
|
||||
## Actualización final
|
||||
|
||||
Para una base ya instalada, ejecutar una sola vez:
|
||||
|
||||
```text
|
||||
actualizacion_final_calendario_y_nomina.sql
|
||||
```
|
||||
|
||||
Este parche agrega los dos contactos regionales de Nómina y actualiza `tax_due_reminders` para que un contacto ubicado en **Regional** con área `nomina` reciba Nómina de todos los países.
|
||||
|
||||
@@ -930,3 +930,451 @@ commit;
|
||||
|
||||
-- Prueba de recordatorios de una fecha:
|
||||
-- select * from public.tax_due_reminders('2026-07-24'::date);
|
||||
|
||||
|
||||
-- ===========================================================================
|
||||
-- ACTUALIZACIÓN JULIO 2026: BambooHR, Google Calendar y fechas futuras
|
||||
-- ===========================================================================
|
||||
-- Seguimiento de Impuestos GLM
|
||||
-- Actualización: contactos BambooHR, edición ampliada, Google Calendar y bloqueo de fechas pasadas.
|
||||
-- Ejecutar una sola vez en Supabase SQL Editor antes de desplegar el frontend y activar los workflows.
|
||||
|
||||
begin;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Contactos: metadatos de BambooHR y tercer canal de notificación
|
||||
-- ---------------------------------------------------------------------------
|
||||
alter table public.tax_contacts
|
||||
add column if not exists department text,
|
||||
add column if not exists division text,
|
||||
add column if not exists location text,
|
||||
add column if not exists source text not null default 'manual',
|
||||
add column if not exists bamboo_employee_id text,
|
||||
add column if not exists bamboo_employee_number text,
|
||||
add column if not exists bamboo_status text,
|
||||
add column if not exists bamboo_synced_at timestamptz,
|
||||
add column if not exists calendar_enabled boolean not null default true;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_area_check;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_area_check
|
||||
check (area in ('admin', 'nomina', 'regional', 'other'));
|
||||
|
||||
update public.tax_contacts
|
||||
set calendar_enabled = false
|
||||
where email is null or length(trim(email)) <= 3;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_calendar_email_when_enabled;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_calendar_email_when_enabled
|
||||
check (not calendar_enabled or length(trim(coalesce(email, ''))) > 3);
|
||||
|
||||
create unique index if not exists tax_contacts_unique_active_bamboo_employee
|
||||
on public.tax_contacts (bamboo_employee_id)
|
||||
where active = true and bamboo_employee_id is not null;
|
||||
|
||||
create index if not exists tax_contacts_bamboo_employee_number_idx
|
||||
on public.tax_contacts (bamboo_employee_number)
|
||||
where bamboo_employee_number is not null;
|
||||
|
||||
-- Amplía la normalización existente para los nuevos campos.
|
||||
create or replace function public.tax_normalize_contact()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
new.full_name := trim(new.full_name);
|
||||
new.email := nullif(lower(trim(coalesce(new.email, ''))), '');
|
||||
new.whatsapp_number := nullif(regexp_replace(coalesce(new.whatsapp_number, ''), '[^0-9]', '', 'g'), '');
|
||||
new.job_title := nullif(trim(coalesce(new.job_title, '')), '');
|
||||
new.department := nullif(trim(coalesce(new.department, '')), '');
|
||||
new.division := nullif(trim(coalesce(new.division, '')), '');
|
||||
new.location := nullif(trim(coalesce(new.location, '')), '');
|
||||
new.source := coalesce(nullif(lower(trim(coalesce(new.source, ''))), ''), 'manual');
|
||||
new.bamboo_employee_id := nullif(trim(coalesce(new.bamboo_employee_id, '')), '');
|
||||
new.bamboo_employee_number := nullif(trim(coalesce(new.bamboo_employee_number, '')), '');
|
||||
new.bamboo_status := nullif(trim(coalesce(new.bamboo_status, '')), '');
|
||||
|
||||
if new.source = 'bamboohr' then
|
||||
if tg_op = 'INSERT' then
|
||||
new.bamboo_synced_at := now();
|
||||
elsif new.bamboo_employee_id is distinct from old.bamboo_employee_id
|
||||
or new.full_name is distinct from old.full_name
|
||||
or new.email is distinct from old.email
|
||||
or new.whatsapp_number is distinct from old.whatsapp_number then
|
||||
new.bamboo_synced_at := now();
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- No permitir obligaciones nuevas o movidas a fechas pasadas
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function public.tax_validate_future_obligation()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
local_today date := (now() at time zone 'America/Santo_Domingo')::date;
|
||||
begin
|
||||
if new.active = true
|
||||
and new.is_historical = false
|
||||
and new.due_date < local_today then
|
||||
if tg_op = 'INSERT' then
|
||||
raise exception 'No se puede crear una obligación en una fecha que ya pasó.'
|
||||
using errcode = '22007';
|
||||
elsif new.due_date is distinct from old.due_date then
|
||||
raise exception 'No se puede mover una obligación a una fecha que ya pasó.'
|
||||
using errcode = '22007';
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists tax_validate_future_obligation_trigger on public.tax_obligations;
|
||||
create trigger tax_validate_future_obligation_trigger
|
||||
before insert or update on public.tax_obligations
|
||||
for each row execute function public.tax_validate_future_obligation();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sincronización con Google Calendar
|
||||
-- Un evento de Google Calendar por obligación, con todos los destinatarios
|
||||
-- aplicables como invitados.
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists public.tax_google_calendar_events (
|
||||
obligation_id uuid primary key references public.tax_obligations(id) on delete cascade,
|
||||
google_event_id text,
|
||||
calendar_id text not null default 'primary',
|
||||
attendees jsonb not null default '[]'::jsonb,
|
||||
status text not null default 'active'
|
||||
check (status in ('active', 'deleted', 'failed')),
|
||||
last_action text,
|
||||
provider_response jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
synced_at timestamptz
|
||||
);
|
||||
|
||||
alter table public.tax_google_calendar_events enable row level security;
|
||||
revoke all on table public.tax_google_calendar_events from anon, authenticated;
|
||||
grant select, insert, update, delete on table public.tax_google_calendar_events to service_role;
|
||||
|
||||
create or replace function public.tax_google_calendar_payload(p_obligation_id uuid)
|
||||
returns table (
|
||||
obligation_id uuid,
|
||||
active boolean,
|
||||
due_date date,
|
||||
description text,
|
||||
category text,
|
||||
country_id uuid,
|
||||
country_name text,
|
||||
country_code text,
|
||||
attendees jsonb,
|
||||
google_event_id text,
|
||||
calendar_id text,
|
||||
sync_status text
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
select
|
||||
obligation.id,
|
||||
obligation.active,
|
||||
obligation.due_date,
|
||||
obligation.description,
|
||||
obligation.category,
|
||||
obligation.country_id,
|
||||
country.name,
|
||||
country.code,
|
||||
coalesce(
|
||||
(
|
||||
select jsonb_agg(recipient.email order by recipient.email)
|
||||
from (
|
||||
select distinct lower(contact.email) as email
|
||||
from public.tax_contacts contact
|
||||
join public.tax_countries contact_country
|
||||
on contact_country.id = contact.country_id
|
||||
where contact.active = true
|
||||
and contact.calendar_enabled = true
|
||||
and contact.email is not null
|
||||
and length(trim(contact.email)) > 3
|
||||
and (
|
||||
contact.area = 'regional'
|
||||
or (
|
||||
contact_country.is_regional = true
|
||||
and contact.area = obligation.category
|
||||
)
|
||||
or (
|
||||
contact.country_id = obligation.country_id
|
||||
and contact.area = obligation.category
|
||||
)
|
||||
)
|
||||
) recipient
|
||||
),
|
||||
'[]'::jsonb
|
||||
) as attendees,
|
||||
sync.google_event_id,
|
||||
coalesce(sync.calendar_id, 'primary') as calendar_id,
|
||||
sync.status
|
||||
from public.tax_obligations obligation
|
||||
join public.tax_countries country on country.id = obligation.country_id
|
||||
left join public.tax_google_calendar_events sync
|
||||
on sync.obligation_id = obligation.id
|
||||
where obligation.id = p_obligation_id;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_google_calendar_payload(uuid)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.tax_google_calendar_payload(uuid)
|
||||
to service_role;
|
||||
|
||||
create or replace function public.tax_record_google_calendar_sync(
|
||||
p_obligation_id uuid,
|
||||
p_google_event_id text,
|
||||
p_calendar_id text default 'primary',
|
||||
p_attendees jsonb default '[]'::jsonb,
|
||||
p_status text default 'active',
|
||||
p_last_action text default null,
|
||||
p_provider_response jsonb default null
|
||||
)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
insert into public.tax_google_calendar_events (
|
||||
obligation_id,
|
||||
google_event_id,
|
||||
calendar_id,
|
||||
attendees,
|
||||
status,
|
||||
last_action,
|
||||
provider_response,
|
||||
synced_at,
|
||||
updated_at
|
||||
)
|
||||
values (
|
||||
p_obligation_id,
|
||||
nullif(trim(coalesce(p_google_event_id, '')), ''),
|
||||
coalesce(nullif(trim(coalesce(p_calendar_id, '')), ''), 'primary'),
|
||||
coalesce(p_attendees, '[]'::jsonb),
|
||||
p_status,
|
||||
p_last_action,
|
||||
p_provider_response,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
on conflict (obligation_id)
|
||||
do update set
|
||||
google_event_id = coalesce(excluded.google_event_id, public.tax_google_calendar_events.google_event_id),
|
||||
calendar_id = excluded.calendar_id,
|
||||
attendees = excluded.attendees,
|
||||
status = excluded.status,
|
||||
last_action = excluded.last_action,
|
||||
provider_response = excluded.provider_response,
|
||||
synced_at = now(),
|
||||
updated_at = now();
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb)
|
||||
to service_role;
|
||||
|
||||
commit;
|
||||
|
||||
-- Verificaciones opcionales:
|
||||
-- select * from public.tax_google_calendar_payload('UUID_DE_OBLIGACION');
|
||||
-- select * from public.tax_google_calendar_events order by updated_at desc;
|
||||
|
||||
|
||||
|
||||
-- Seguimiento de Impuestos GLM
|
||||
-- Actualización incremental: retirar WhatsApp y habilitar Google Chat.
|
||||
-- Ejecutar después de actualizacion_bamboohr_google_calendar.sql.
|
||||
|
||||
begin;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add column if not exists google_chat_enabled boolean not null default true;
|
||||
|
||||
alter table public.tax_contacts
|
||||
alter column whatsapp_enabled set default false;
|
||||
|
||||
-- La nueva operación deja de usar WhatsApp. Se conserva la información histórica
|
||||
-- para no destruir datos ni bitácoras anteriores.
|
||||
update public.tax_contacts
|
||||
set
|
||||
whatsapp_enabled = false,
|
||||
google_chat_enabled = case
|
||||
when email_enabled = true and email is not null and length(trim(email)) > 3 then true
|
||||
else false
|
||||
end;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_google_chat_email_when_enabled;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_google_chat_email_when_enabled
|
||||
check (not google_chat_enabled or length(trim(coalesce(email, ''))) > 3);
|
||||
|
||||
alter table public.tax_notification_log
|
||||
drop constraint if exists tax_notification_log_channel_check;
|
||||
|
||||
alter table public.tax_notification_log
|
||||
add constraint tax_notification_log_channel_check
|
||||
check (channel in ('email', 'google_chat', 'whatsapp'));
|
||||
|
||||
-- CREATE OR REPLACE no permite cambiar las columnas retornadas, por eso se elimina
|
||||
-- y se recrea la RPC usada por n8n.
|
||||
drop function if exists public.tax_due_reminders(date);
|
||||
|
||||
create function public.tax_due_reminders(p_run_date date default current_date)
|
||||
returns table (
|
||||
obligation_id uuid,
|
||||
contact_id uuid,
|
||||
alert_date date,
|
||||
alert_type text,
|
||||
due_date date,
|
||||
description text,
|
||||
category text,
|
||||
country_name text,
|
||||
country_code text,
|
||||
contact_name text,
|
||||
contact_email text,
|
||||
email_enabled boolean,
|
||||
google_chat_enabled boolean
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
with due as (
|
||||
select
|
||||
obligation.id,
|
||||
obligation.due_date,
|
||||
obligation.description,
|
||||
obligation.category,
|
||||
obligation.country_id,
|
||||
country.name as country_name,
|
||||
country.code as country_code,
|
||||
case
|
||||
when p_run_date = obligation.due_date then 'same_day'
|
||||
else 'previous_wednesday'
|
||||
end as alert_type
|
||||
from public.tax_obligations obligation
|
||||
join public.tax_countries country on country.id = obligation.country_id
|
||||
where obligation.active = true
|
||||
and (
|
||||
p_run_date = obligation.due_date
|
||||
or p_run_date = public.tax_previous_wednesday(obligation.due_date)
|
||||
)
|
||||
)
|
||||
select
|
||||
due.id as obligation_id,
|
||||
contact.id as contact_id,
|
||||
p_run_date as alert_date,
|
||||
due.alert_type,
|
||||
due.due_date,
|
||||
due.description,
|
||||
due.category,
|
||||
due.country_name,
|
||||
due.country_code,
|
||||
contact.full_name as contact_name,
|
||||
contact.email as contact_email,
|
||||
(
|
||||
contact.email_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'email'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
) as email_enabled,
|
||||
(
|
||||
contact.google_chat_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'google_chat'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
) as google_chat_enabled
|
||||
from due
|
||||
join public.tax_contacts contact on contact.active = true
|
||||
join public.tax_countries contact_country
|
||||
on contact_country.id = contact.country_id
|
||||
and (
|
||||
contact.area = 'regional'
|
||||
or (
|
||||
contact_country.is_regional = true
|
||||
and contact.area = due.category
|
||||
)
|
||||
or (
|
||||
contact.country_id = due.country_id
|
||||
and contact.area = due.category
|
||||
)
|
||||
)
|
||||
where
|
||||
(
|
||||
contact.email_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'email'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
)
|
||||
or
|
||||
(
|
||||
contact.google_chat_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'google_chat'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
)
|
||||
order by due.due_date, due.country_name, contact.full_name;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_due_reminders(date) from public, anon, authenticated;
|
||||
grant execute on function public.tax_due_reminders(date) to service_role;
|
||||
|
||||
commit;
|
||||
|
||||
-- Verificación opcional:
|
||||
-- select full_name, email_enabled, google_chat_enabled, calendar_enabled, whatsapp_enabled
|
||||
-- from public.tax_contacts where active = true order by full_name;
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
-- Seguimiento de Impuestos GLM
|
||||
-- Actualización: contactos BambooHR, edición ampliada, Google Calendar y bloqueo de fechas pasadas.
|
||||
-- Ejecutar una sola vez en Supabase SQL Editor antes de desplegar el frontend y activar los workflows.
|
||||
|
||||
begin;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Contactos: metadatos de BambooHR y tercer canal de notificación
|
||||
-- ---------------------------------------------------------------------------
|
||||
alter table public.tax_contacts
|
||||
add column if not exists department text,
|
||||
add column if not exists division text,
|
||||
add column if not exists location text,
|
||||
add column if not exists source text not null default 'manual',
|
||||
add column if not exists bamboo_employee_id text,
|
||||
add column if not exists bamboo_employee_number text,
|
||||
add column if not exists bamboo_status text,
|
||||
add column if not exists bamboo_synced_at timestamptz,
|
||||
add column if not exists calendar_enabled boolean not null default true;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_area_check;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_area_check
|
||||
check (area in ('admin', 'nomina', 'regional', 'other'));
|
||||
|
||||
update public.tax_contacts
|
||||
set calendar_enabled = false
|
||||
where email is null or length(trim(email)) <= 3;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_calendar_email_when_enabled;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_calendar_email_when_enabled
|
||||
check (not calendar_enabled or length(trim(coalesce(email, ''))) > 3);
|
||||
|
||||
create unique index if not exists tax_contacts_unique_active_bamboo_employee
|
||||
on public.tax_contacts (bamboo_employee_id)
|
||||
where active = true and bamboo_employee_id is not null;
|
||||
|
||||
create index if not exists tax_contacts_bamboo_employee_number_idx
|
||||
on public.tax_contacts (bamboo_employee_number)
|
||||
where bamboo_employee_number is not null;
|
||||
|
||||
-- Amplía la normalización existente para los nuevos campos.
|
||||
create or replace function public.tax_normalize_contact()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
new.full_name := trim(new.full_name);
|
||||
new.email := nullif(lower(trim(coalesce(new.email, ''))), '');
|
||||
new.whatsapp_number := nullif(regexp_replace(coalesce(new.whatsapp_number, ''), '[^0-9]', '', 'g'), '');
|
||||
new.job_title := nullif(trim(coalesce(new.job_title, '')), '');
|
||||
new.department := nullif(trim(coalesce(new.department, '')), '');
|
||||
new.division := nullif(trim(coalesce(new.division, '')), '');
|
||||
new.location := nullif(trim(coalesce(new.location, '')), '');
|
||||
new.source := coalesce(nullif(lower(trim(coalesce(new.source, ''))), ''), 'manual');
|
||||
new.bamboo_employee_id := nullif(trim(coalesce(new.bamboo_employee_id, '')), '');
|
||||
new.bamboo_employee_number := nullif(trim(coalesce(new.bamboo_employee_number, '')), '');
|
||||
new.bamboo_status := nullif(trim(coalesce(new.bamboo_status, '')), '');
|
||||
|
||||
if new.source = 'bamboohr' then
|
||||
if tg_op = 'INSERT' then
|
||||
new.bamboo_synced_at := now();
|
||||
elsif new.bamboo_employee_id is distinct from old.bamboo_employee_id
|
||||
or new.full_name is distinct from old.full_name
|
||||
or new.email is distinct from old.email
|
||||
or new.whatsapp_number is distinct from old.whatsapp_number then
|
||||
new.bamboo_synced_at := now();
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- No permitir obligaciones nuevas o movidas a fechas pasadas
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function public.tax_validate_future_obligation()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
local_today date := (now() at time zone 'America/Santo_Domingo')::date;
|
||||
begin
|
||||
if new.active = true
|
||||
and new.is_historical = false
|
||||
and new.due_date < local_today then
|
||||
if tg_op = 'INSERT' then
|
||||
raise exception 'No se puede crear una obligación en una fecha que ya pasó.'
|
||||
using errcode = '22007';
|
||||
elsif new.due_date is distinct from old.due_date then
|
||||
raise exception 'No se puede mover una obligación a una fecha que ya pasó.'
|
||||
using errcode = '22007';
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists tax_validate_future_obligation_trigger on public.tax_obligations;
|
||||
create trigger tax_validate_future_obligation_trigger
|
||||
before insert or update on public.tax_obligations
|
||||
for each row execute function public.tax_validate_future_obligation();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sincronización con Google Calendar
|
||||
-- Un evento de Google Calendar por obligación, con todos los destinatarios
|
||||
-- aplicables como invitados.
|
||||
-- ---------------------------------------------------------------------------
|
||||
create table if not exists public.tax_google_calendar_events (
|
||||
obligation_id uuid primary key references public.tax_obligations(id) on delete cascade,
|
||||
google_event_id text,
|
||||
calendar_id text not null default 'primary',
|
||||
attendees jsonb not null default '[]'::jsonb,
|
||||
status text not null default 'active'
|
||||
check (status in ('active', 'deleted', 'failed')),
|
||||
last_action text,
|
||||
provider_response jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
synced_at timestamptz
|
||||
);
|
||||
|
||||
alter table public.tax_google_calendar_events enable row level security;
|
||||
revoke all on table public.tax_google_calendar_events from anon, authenticated;
|
||||
grant select, insert, update, delete on table public.tax_google_calendar_events to service_role;
|
||||
|
||||
create or replace function public.tax_google_calendar_payload(p_obligation_id uuid)
|
||||
returns table (
|
||||
obligation_id uuid,
|
||||
active boolean,
|
||||
due_date date,
|
||||
description text,
|
||||
category text,
|
||||
country_id uuid,
|
||||
country_name text,
|
||||
country_code text,
|
||||
attendees jsonb,
|
||||
google_event_id text,
|
||||
calendar_id text,
|
||||
sync_status text
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
select
|
||||
obligation.id,
|
||||
obligation.active,
|
||||
obligation.due_date,
|
||||
obligation.description,
|
||||
obligation.category,
|
||||
obligation.country_id,
|
||||
country.name,
|
||||
country.code,
|
||||
coalesce(
|
||||
(
|
||||
select jsonb_agg(recipient.email order by recipient.email)
|
||||
from (
|
||||
select distinct lower(contact.email) as email
|
||||
from public.tax_contacts contact
|
||||
join public.tax_countries contact_country
|
||||
on contact_country.id = contact.country_id
|
||||
where contact.active = true
|
||||
and contact.calendar_enabled = true
|
||||
and contact.email is not null
|
||||
and length(trim(contact.email)) > 3
|
||||
and (
|
||||
contact.area = 'regional'
|
||||
or (
|
||||
contact_country.is_regional = true
|
||||
and contact.area = obligation.category
|
||||
)
|
||||
or (
|
||||
contact.country_id = obligation.country_id
|
||||
and contact.area = obligation.category
|
||||
)
|
||||
)
|
||||
) recipient
|
||||
),
|
||||
'[]'::jsonb
|
||||
) as attendees,
|
||||
sync.google_event_id,
|
||||
coalesce(sync.calendar_id, 'primary') as calendar_id,
|
||||
sync.status
|
||||
from public.tax_obligations obligation
|
||||
join public.tax_countries country on country.id = obligation.country_id
|
||||
left join public.tax_google_calendar_events sync
|
||||
on sync.obligation_id = obligation.id
|
||||
where obligation.id = p_obligation_id;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_google_calendar_payload(uuid)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.tax_google_calendar_payload(uuid)
|
||||
to service_role;
|
||||
|
||||
create or replace function public.tax_record_google_calendar_sync(
|
||||
p_obligation_id uuid,
|
||||
p_google_event_id text,
|
||||
p_calendar_id text default 'primary',
|
||||
p_attendees jsonb default '[]'::jsonb,
|
||||
p_status text default 'active',
|
||||
p_last_action text default null,
|
||||
p_provider_response jsonb default null
|
||||
)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
insert into public.tax_google_calendar_events (
|
||||
obligation_id,
|
||||
google_event_id,
|
||||
calendar_id,
|
||||
attendees,
|
||||
status,
|
||||
last_action,
|
||||
provider_response,
|
||||
synced_at,
|
||||
updated_at
|
||||
)
|
||||
values (
|
||||
p_obligation_id,
|
||||
nullif(trim(coalesce(p_google_event_id, '')), ''),
|
||||
coalesce(nullif(trim(coalesce(p_calendar_id, '')), ''), 'primary'),
|
||||
coalesce(p_attendees, '[]'::jsonb),
|
||||
p_status,
|
||||
p_last_action,
|
||||
p_provider_response,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
on conflict (obligation_id)
|
||||
do update set
|
||||
google_event_id = coalesce(excluded.google_event_id, public.tax_google_calendar_events.google_event_id),
|
||||
calendar_id = excluded.calendar_id,
|
||||
attendees = excluded.attendees,
|
||||
status = excluded.status,
|
||||
last_action = excluded.last_action,
|
||||
provider_response = excluded.provider_response,
|
||||
synced_at = now(),
|
||||
updated_at = now();
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.tax_record_google_calendar_sync(uuid, text, text, jsonb, text, text, jsonb)
|
||||
to service_role;
|
||||
|
||||
commit;
|
||||
|
||||
-- Verificaciones opcionales:
|
||||
-- select * from public.tax_google_calendar_payload('UUID_DE_OBLIGACION');
|
||||
-- select * from public.tax_google_calendar_events order by updated_at desc;
|
||||
@@ -0,0 +1,174 @@
|
||||
-- Seguimiento de Impuestos GLM
|
||||
-- Actualización incremental: retirar WhatsApp y habilitar Google Chat.
|
||||
-- Ejecutar después de actualizacion_bamboohr_google_calendar.sql.
|
||||
|
||||
begin;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add column if not exists google_chat_enabled boolean not null default true;
|
||||
|
||||
alter table public.tax_contacts
|
||||
alter column whatsapp_enabled set default false;
|
||||
|
||||
-- La nueva operación deja de usar WhatsApp. Se conserva la información histórica
|
||||
-- para no destruir datos ni bitácoras anteriores.
|
||||
update public.tax_contacts
|
||||
set
|
||||
whatsapp_enabled = false,
|
||||
google_chat_enabled = case
|
||||
when email_enabled = true and email is not null and length(trim(email)) > 3 then true
|
||||
else false
|
||||
end;
|
||||
|
||||
alter table public.tax_contacts
|
||||
drop constraint if exists tax_contacts_google_chat_email_when_enabled;
|
||||
|
||||
alter table public.tax_contacts
|
||||
add constraint tax_contacts_google_chat_email_when_enabled
|
||||
check (not google_chat_enabled or length(trim(coalesce(email, ''))) > 3);
|
||||
|
||||
alter table public.tax_notification_log
|
||||
drop constraint if exists tax_notification_log_channel_check;
|
||||
|
||||
alter table public.tax_notification_log
|
||||
add constraint tax_notification_log_channel_check
|
||||
check (channel in ('email', 'google_chat', 'whatsapp'));
|
||||
|
||||
-- CREATE OR REPLACE no permite cambiar las columnas retornadas, por eso se elimina
|
||||
-- y se recrea la RPC usada por n8n.
|
||||
drop function if exists public.tax_due_reminders(date);
|
||||
|
||||
create function public.tax_due_reminders(p_run_date date default current_date)
|
||||
returns table (
|
||||
obligation_id uuid,
|
||||
contact_id uuid,
|
||||
alert_date date,
|
||||
alert_type text,
|
||||
due_date date,
|
||||
description text,
|
||||
category text,
|
||||
country_name text,
|
||||
country_code text,
|
||||
contact_name text,
|
||||
contact_email text,
|
||||
email_enabled boolean,
|
||||
google_chat_enabled boolean
|
||||
)
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
with due as (
|
||||
select
|
||||
obligation.id,
|
||||
obligation.due_date,
|
||||
obligation.description,
|
||||
obligation.category,
|
||||
obligation.country_id,
|
||||
country.name as country_name,
|
||||
country.code as country_code,
|
||||
case
|
||||
when p_run_date = obligation.due_date then 'same_day'
|
||||
else 'previous_wednesday'
|
||||
end as alert_type
|
||||
from public.tax_obligations obligation
|
||||
join public.tax_countries country on country.id = obligation.country_id
|
||||
where obligation.active = true
|
||||
and (
|
||||
p_run_date = obligation.due_date
|
||||
or p_run_date = public.tax_previous_wednesday(obligation.due_date)
|
||||
)
|
||||
)
|
||||
select
|
||||
due.id as obligation_id,
|
||||
contact.id as contact_id,
|
||||
p_run_date as alert_date,
|
||||
due.alert_type,
|
||||
due.due_date,
|
||||
due.description,
|
||||
due.category,
|
||||
due.country_name,
|
||||
due.country_code,
|
||||
contact.full_name as contact_name,
|
||||
contact.email as contact_email,
|
||||
(
|
||||
contact.email_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'email'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
) as email_enabled,
|
||||
(
|
||||
contact.google_chat_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'google_chat'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
) as google_chat_enabled
|
||||
from due
|
||||
join public.tax_contacts contact on contact.active = true
|
||||
join public.tax_countries contact_country
|
||||
on contact_country.id = contact.country_id
|
||||
and (
|
||||
contact.area = 'regional'
|
||||
or (
|
||||
contact_country.is_regional = true
|
||||
and contact.area = due.category
|
||||
)
|
||||
or (
|
||||
contact.country_id = due.country_id
|
||||
and contact.area = due.category
|
||||
)
|
||||
)
|
||||
where
|
||||
(
|
||||
contact.email_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'email'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
)
|
||||
or
|
||||
(
|
||||
contact.google_chat_enabled
|
||||
and contact.email is not null
|
||||
and not exists (
|
||||
select 1
|
||||
from public.tax_notification_log log
|
||||
where log.obligation_id = due.id
|
||||
and log.contact_id = contact.id
|
||||
and log.alert_date = p_run_date
|
||||
and log.channel = 'google_chat'
|
||||
and log.status = 'sent'
|
||||
)
|
||||
)
|
||||
order by due.due_date, due.country_name, contact.full_name;
|
||||
$$;
|
||||
|
||||
revoke all on function public.tax_due_reminders(date) from public, anon, authenticated;
|
||||
grant execute on function public.tax_due_reminders(date) to service_role;
|
||||
|
||||
commit;
|
||||
|
||||
-- Verificación opcional:
|
||||
-- select full_name, email_enabled, google_chat_enabled, calendar_enabled, whatsapp_enabled
|
||||
-- from public.tax_contacts where active = true order by full_name;
|
||||