Versión inicial de Seguimiento de Impuestos GLM
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# URL pública de Supabase. No agregues una barra al final.
|
||||||
|
VITE_SUPABASE_URL=https://dbit.digitalcompass.agency
|
||||||
|
|
||||||
|
# Usa una llave pública/publishable. Nunca coloques la service_role en el frontend.
|
||||||
|
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.
|
||||||
|
VITE_TAX_WEBHOOK_URL=https://agenteit.digitalcompass.agency/webhook/seguimiento-impuestos
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Local environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editors and operating system
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Guía de mantenimiento
|
||||||
|
|
||||||
|
- Mantener intacta la identidad visual aprobada de GLM salvo solicitud explícita.
|
||||||
|
- Usar Arial y la paleta oficial GLM en la app y en los correos.
|
||||||
|
- No incorporar dependencias sin una necesidad funcional concreta.
|
||||||
|
- Supabase es la fuente única de verdad para países, obligaciones, contactos y bitácora de envíos.
|
||||||
|
- No duplicar datos operativos dentro de `src/`: Supabase es la fuente única de verdad.
|
||||||
|
- Mantener la autenticación con Google y el control de acceso en `public.tax_calendar_access`.
|
||||||
|
- 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.
|
||||||
|
- Antes de entregar cambios, ejecutar `npm run typecheck`, `npm run lint` y `npm run build`.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Seguimiento de Impuestos GLM
|
||||||
|
|
||||||
|
Aplicación interna para mantener el calendario tributario regional, administrar destinatarios y alimentar el flujo automático de recordatorios por Gmail y WhatsApp.
|
||||||
|
|
||||||
|
## Incluye
|
||||||
|
|
||||||
|
- Login con Google y control de acceso desde Supabase.
|
||||||
|
- Calendario histórico 2026 migrado a PostgreSQL/Supabase.
|
||||||
|
- Creación, consulta, edición y eliminación lógica de obligaciones antes de su primer aviso.
|
||||||
|
- Bloqueo automático de edición y eliminación después del primer recordatorio enviado.
|
||||||
|
- País obligatorio y categorías limitadas a **Nómina** y **Administración**.
|
||||||
|
- Regla de alertas: **miércoles estrictamente anterior y el mismo día**, incluidos sábados y domingos.
|
||||||
|
- Contactos almacenados en Supabase y administrables desde la app.
|
||||||
|
- Países visibles aunque todavía no tengan contactos.
|
||||||
|
- Filtro buscable por país y búsqueda por nombre en Contactos.
|
||||||
|
- Búsqueda por nombre o país en Lista.
|
||||||
|
- Webhook de producción configurado para n8n.
|
||||||
|
- Workflow de n8n para Gmail + WhatsApp con correo HTML GLM.
|
||||||
|
|
||||||
|
## 1. Crear la base de datos
|
||||||
|
|
||||||
|
Ejecuta completo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supabase/Seguimiento-de-Impuestos-GLM.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
El script crea tablas, RLS, funciones, bitácora anti-duplicados, bloqueo de eventos notificados, accesos iniciales, contactos y las 294 obligaciones históricas de 2026.
|
||||||
|
|
||||||
|
Si ya habías ejecutado una versión anterior del script completo, ejecuta solamente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supabase/actualizacion_eventos_bloqueados.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Configurar frontend
|
||||||
|
|
||||||
|
Copia `.env.example` como `.env` y completa la llave pública de Supabase. La `service_role` nunca debe ponerse en Vite.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
URL local:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173/calendario-impuestos/
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Configurar n8n
|
||||||
|
|
||||||
|
Importa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
n8n/Seguimiento-de-Impuestos-GLM.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Luego sigue `n8n/CONFIGURACION.md`. El único ajuste dentro del canvas es seleccionar la credencial de Gmail; las demás claves se leen de variables de entorno.
|
||||||
|
|
||||||
|
## Enrutamiento
|
||||||
|
|
||||||
|
- **Regional** recibe todas las obligaciones.
|
||||||
|
- **Nómina** recibe únicamente obligaciones de Nómina de su país.
|
||||||
|
- **Administración** recibe únicamente obligaciones de Administración de su país.
|
||||||
|
- El flujo consulta los contactos en Supabase en cada ejecución; agregar un contacto en la app no requiere modificar n8n.
|
||||||
|
|
||||||
|
## Despliegue
|
||||||
|
|
||||||
|
Vite conserva la base:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
base: "/calendario-impuestos/";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actualización final de calendario y Nómina
|
||||||
|
|
||||||
|
- Cuando un día tiene más de tres obligaciones, el enlace **Ver todos** abre una vista con la lista completa; desde allí se puede abrir cada obligación.
|
||||||
|
- El script `supabase/actualizacion_final_calendario_y_nomina.sql` agrega a Mati Soto Valenzuela e Iveth Herrera como contactos regionales especializados en Nómina.
|
||||||
|
- Los contactos regionales con área `nomina` reciben obligaciones de Nómina de todos los países. Los contactos con área `regional` continúan recibiendo todas las categorías.
|
||||||
|
- El número de Mati se guarda como `18094037571`, agregando el prefijo internacional `1` requerido para WhatsApp.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist", "node_modules"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eslintPluginPrettier,
|
||||||
|
);
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<!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="%BASE_URL%favicon-32.png?v=glm-2026" />
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192" href="%BASE_URL%favicon-192.png?v=glm-2026" />
|
||||||
|
<link rel="shortcut icon" href="%BASE_URL%favicon.ico?v=glm-2026" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="%BASE_URL%apple-touch-icon.png?v=glm-2026" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Configuración del flujo n8n
|
||||||
|
|
||||||
|
Después de importar `Seguimiento-de-Impuestos-GLM.json`:
|
||||||
|
|
||||||
|
1. En el nodo **Gmail - Enviar recordatorio**, selecciona la credencial de Gmail que enviará los correos.
|
||||||
|
2. Configura estas variables de entorno en n8n:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "run_now",
|
||||||
|
"run_date": "2026-07-24"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Las notificaciones del frontend (`contact.created`, `obligation.created`, etc.) sólo confirman cambios y no disparan recordatorios.
|
||||||
|
|
||||||
|
## Enrutamiento regional por categoría
|
||||||
|
|
||||||
|
El workflow no requiere cambios para los nuevos contactos de Nómina. La RPC `tax_due_reminders` devuelve automáticamente:
|
||||||
|
|
||||||
|
- 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.
|
||||||
File diff suppressed because one or more lines are too long
Generated
+3812
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "seguimiento-impuestos-glm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.32.0",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@types/react": "^19.2.0",
|
||||||
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
|
"eslint": "^9.32.0",
|
||||||
|
"eslint-config-prettier": "^10.1.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^15.15.0",
|
||||||
|
"prettier": "^3.7.3",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.56.1",
|
||||||
|
"vite": "^8.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 767 B |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
import AuthGate from "./auth/AuthGate";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <AuthGate />;
|
||||||
|
}
|
||||||
+1878
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,309 @@
|
|||||||
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
|
import glmLogo from "../assets/glm-logo.png";
|
||||||
|
import TaxApp from "../TaxApp";
|
||||||
|
import {
|
||||||
|
isSupabaseConfigured,
|
||||||
|
restoreSession,
|
||||||
|
signInWithGoogle,
|
||||||
|
signOut,
|
||||||
|
verifyTaxCalendarAccess,
|
||||||
|
type SupabaseAuthSession,
|
||||||
|
} from "../lib/supabase-auth";
|
||||||
|
|
||||||
|
type AuthState =
|
||||||
|
| { status: "loading" }
|
||||||
|
| { status: "signed-out"; error?: string }
|
||||||
|
| { status: "checking"; session: SupabaseAuthSession }
|
||||||
|
| { status: "authorized"; session: SupabaseAuthSession }
|
||||||
|
| { status: "denied"; session: SupabaseAuthSession; error?: string }
|
||||||
|
| { status: "configuration" };
|
||||||
|
|
||||||
|
export default function AuthGate() {
|
||||||
|
const [state, setState] = useState<AuthState>({ status: "loading" });
|
||||||
|
const [actionPending, setActionPending] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
if (!isSupabaseConfigured) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setState({ status: "configuration" });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await restoreSession();
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
setState({ status: "signed-out" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({ status: "checking", session });
|
||||||
|
const result = await verifyTaxCalendarAccess(session);
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
setState(
|
||||||
|
result.allowed
|
||||||
|
? { status: "authorized", session: result.session }
|
||||||
|
: { status: "denied", session: result.session },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setState({
|
||||||
|
status: "signed-out",
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void initialize();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRecheck = async (session: SupabaseAuthSession) => {
|
||||||
|
setActionPending(true);
|
||||||
|
setState({ status: "checking", session });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await verifyTaxCalendarAccess(session);
|
||||||
|
setState(
|
||||||
|
result.allowed
|
||||||
|
? { status: "authorized", session: result.session }
|
||||||
|
: { status: "denied", session: result.session },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setState({
|
||||||
|
status: "denied",
|
||||||
|
session,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setActionPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
setActionPending(true);
|
||||||
|
try {
|
||||||
|
signInWithGoogle();
|
||||||
|
} catch (error) {
|
||||||
|
setActionPending(false);
|
||||||
|
setState({ status: "signed-out", error: getErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignOut = async (session?: SupabaseAuthSession) => {
|
||||||
|
setActionPending(true);
|
||||||
|
await signOut(session);
|
||||||
|
setActionPending(false);
|
||||||
|
setState({ status: "signed-out" });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.status === "authorized") {
|
||||||
|
return <TaxApp session={state.session} onSignOut={() => handleSignOut(state.session)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "loading" || state.status === "checking") {
|
||||||
|
return <LoadingScreen checkingAccess={state.status === "checking"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "configuration") {
|
||||||
|
return (
|
||||||
|
<AuthShell>
|
||||||
|
<StatusPanel
|
||||||
|
eyebrow="Servicio no disponible"
|
||||||
|
title="No pudimos abrir la aplicación"
|
||||||
|
description="La aplicación no está disponible en este momento."
|
||||||
|
detail="Comunícate con el equipo de IT para recibir asistencia."
|
||||||
|
/>
|
||||||
|
</AuthShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === "denied") {
|
||||||
|
const verificationFailed = Boolean(state.error);
|
||||||
|
return (
|
||||||
|
<AuthShell>
|
||||||
|
<StatusPanel
|
||||||
|
eyebrow={verificationFailed ? "No se pudo validar" : "Acceso restringido"}
|
||||||
|
title={
|
||||||
|
verificationFailed ? "No pudimos comprobar tu acceso" : "Este correo no tiene acceso"
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
verificationFailed
|
||||||
|
? (state.error ?? "No pudimos completar la validación.")
|
||||||
|
: "Tu cuenta no tiene acceso a esta aplicación."
|
||||||
|
}
|
||||||
|
detail={
|
||||||
|
verificationFailed
|
||||||
|
? "Inténtalo nuevamente en unos minutos o usa otra cuenta."
|
||||||
|
: "Comunícate con el equipo de IT si necesitas solicitar acceso."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRecheck(state.session)}
|
||||||
|
disabled={actionPending}
|
||||||
|
className="w-full rounded-lg bg-glm-green px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-glm-green/90 disabled:cursor-wait disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Volver a comprobar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSignOut(state.session)}
|
||||||
|
disabled={actionPending}
|
||||||
|
className="w-full rounded-lg border border-border bg-card px-4 py-2.5 text-sm font-semibold text-glm-blue transition hover:bg-muted disabled:cursor-wait disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Usar otra cuenta
|
||||||
|
</button>
|
||||||
|
</StatusPanel>
|
||||||
|
</AuthShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthShell>
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<div className="mb-8">
|
||||||
|
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-glm-green">
|
||||||
|
Acceso interno
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight text-glm-blue sm:text-4xl">
|
||||||
|
Seguimiento de Impuestos GLM
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||||
|
Consulta y administra el calendario tributario regional desde un solo lugar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{state.error && (
|
||||||
|
<div className="mb-4 rounded-lg border border-destructive/25 bg-destructive/5 px-4 py-3 text-sm text-destructive">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogin}
|
||||||
|
disabled={actionPending}
|
||||||
|
className="flex w-full items-center justify-center gap-3 rounded-lg border border-border bg-card px-4 py-3 text-sm font-semibold text-foreground shadow-sm transition hover:-translate-y-px hover:border-glm-blue/35 hover:shadow-md disabled:cursor-wait disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<GoogleIcon />
|
||||||
|
{actionPending ? "Abriendo Google…" : "Continuar con Google"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</AuthShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthShell({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-5 py-10">
|
||||||
|
<div className="pointer-events-none absolute inset-0 auth-grid opacity-55" />
|
||||||
|
<div className="pointer-events-none absolute -left-28 top-1/4 h-72 w-72 rounded-full bg-glm-green/10 blur-3xl" />
|
||||||
|
<div className="pointer-events-none absolute -right-24 bottom-1/4 h-80 w-80 rounded-full bg-glm-blue/10 blur-3xl" />
|
||||||
|
|
||||||
|
<section className="relative grid w-full max-w-5xl overflow-hidden rounded-3xl border border-border bg-card shadow-[0_28px_90px_rgba(79,117,139,0.16)] lg:grid-cols-[0.9fr_1.1fr]">
|
||||||
|
<div className="relative hidden min-h-[560px] border-r border-border bg-muted/35 p-10 lg:flex lg:flex-col lg:justify-between">
|
||||||
|
<img src={glmLogo} alt="GomezLee Marketing" className="h-auto w-44" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 h-1 w-14 rounded-full bg-glm-green" />
|
||||||
|
<p className="max-w-xs text-2xl font-semibold leading-tight text-glm-blue">
|
||||||
|
Obligaciones tributarias claras, organizadas y listas para dar seguimiento.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 max-w-sm text-sm leading-6 text-muted-foreground">
|
||||||
|
Calendario regional de GomezLee Marketing con fechas históricas, contactos y alertas
|
||||||
|
calculadas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">GomezLee Marketing · Uso interno</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex min-h-[520px] items-center justify-center p-7 sm:p-12 lg:min-h-[560px] lg:p-16">
|
||||||
|
<div className="w-full">
|
||||||
|
<img src={glmLogo} alt="GomezLee Marketing" className="mb-10 h-auto w-36 lg:hidden" />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingScreen({ checkingAccess }: { checkingAccess: boolean }) {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen items-center justify-center bg-background px-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<img src={glmLogo} alt="GomezLee Marketing" className="mx-auto h-auto w-40" />
|
||||||
|
<div className="mx-auto mt-8 h-8 w-8 animate-spin rounded-full border-2 border-glm-blue/20 border-t-glm-green" />
|
||||||
|
<p className="mt-4 text-sm text-muted-foreground">
|
||||||
|
{checkingAccess ? "Verificando acceso…" : "Cargando aplicación…"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusPanel({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
detail,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
eyebrow: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
detail: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-glm-green">
|
||||||
|
{eyebrow}
|
||||||
|
</p>
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight text-glm-blue">{title}</h1>
|
||||||
|
<p className="mt-4 text-sm leading-6 text-foreground">{description}</p>
|
||||||
|
<p className="mt-2 text-xs leading-5 text-muted-foreground">{detail}</p>
|
||||||
|
{children && <div className="mt-7 space-y-3">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoogleIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="19" height="19" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M21.6 12.23c0-.71-.06-1.23-.2-1.78H12v3.4h5.52a4.75 4.75 0 0 1-2.05 3.03l-.02.11 2.98 2.31.21.02c1.91-1.76 2.96-4.35 2.96-7.09Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M12 22c2.72 0 5-.9 6.65-2.44l-3.17-2.45c-.85.59-1.99 1-3.48 1a6.04 6.04 0 0 1-5.72-4.17l-.1.01-3.1 2.4-.04.1A10.04 10.04 0 0 0 12 22Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M6.28 13.94A6.2 6.2 0 0 1 5.94 12c0-.67.12-1.32.33-1.94l-.01-.13-3.14-2.44-.1.05A10.02 10.02 0 0 0 2 12c0 1.61.38 3.13 1.04 4.46l3.24-2.52Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M12 5.89c1.89 0 3.16.82 3.88 1.5l2.84-2.77C16.98 3 14.72 2 12 2a10.04 10.04 0 0 0-8.97 5.54l3.24 2.52A6.07 6.07 0 0 1 12 5.89Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
void error;
|
||||||
|
return "No pudimos completar el proceso. Inténtalo nuevamente.";
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
export type SupabaseAuthUser = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
user_metadata?: {
|
||||||
|
avatar_url?: string;
|
||||||
|
full_name?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SupabaseAuthSession = {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
expiresAt: number;
|
||||||
|
user: SupabaseAuthUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TokenResponse = {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
expires_at?: number;
|
||||||
|
expires_in?: number;
|
||||||
|
user?: SupabaseAuthUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const supabaseUrl = String(import.meta.env.VITE_SUPABASE_URL ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
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 SESSION_STORAGE_KEY = "glm-tax-auth-session-v1";
|
||||||
|
const EXPIRY_MARGIN_SECONDS = 60;
|
||||||
|
|
||||||
|
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseKey);
|
||||||
|
|
||||||
|
export function getSupabaseConfigurationMessage(): string | null {
|
||||||
|
if (isSupabaseConfigured) return null;
|
||||||
|
|
||||||
|
const missing: string[] = [];
|
||||||
|
if (!supabaseUrl) missing.push("VITE_SUPABASE_URL");
|
||||||
|
if (!supabaseKey) {
|
||||||
|
missing.push("VITE_SUPABASE_PUBLISHABLE_KEY o VITE_SUPABASE_ANON_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Falta configurar ${missing.join(" y ")} en el archivo .env.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOAuthRedirectUrl(): string {
|
||||||
|
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signInWithGoogle(): void {
|
||||||
|
assertConfigured();
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
provider: "google",
|
||||||
|
redirect_to: getOAuthRedirectUrl(),
|
||||||
|
prompt: "select_account",
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.assign(`${supabaseUrl}/auth/v1/authorize?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreSession(): Promise<SupabaseAuthSession | null> {
|
||||||
|
assertConfigured();
|
||||||
|
|
||||||
|
const callbackSession = await consumeOAuthCallback();
|
||||||
|
if (callbackSession) return callbackSession;
|
||||||
|
|
||||||
|
const stored = readStoredSession();
|
||||||
|
if (!stored) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await ensureFreshSession(stored);
|
||||||
|
} catch {
|
||||||
|
clearStoredSession();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyTaxCalendarAccess(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
): Promise<{ allowed: boolean; session: SupabaseAuthSession }> {
|
||||||
|
const freshSession = await ensureFreshSession(session);
|
||||||
|
const response = await fetch(`${supabaseUrl}/rest/v1/rpc/has_tax_calendar_access`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: authenticatedHeaders(freshSession.accessToken),
|
||||||
|
body: "{}",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getResponseError(response, "No se pudo verificar el acceso."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = (await response.json()) === true;
|
||||||
|
return { allowed, session: freshSession };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signOut(session?: SupabaseAuthSession | null): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (session?.accessToken && isSupabaseConfigured) {
|
||||||
|
await fetch(`${supabaseUrl}/auth/v1/logout`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: authenticatedHeaders(session.accessToken),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// El cierre local debe completarse incluso si Supabase no está disponible.
|
||||||
|
} finally {
|
||||||
|
clearStoredSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consumeOAuthCallback(): Promise<SupabaseAuthSession | null> {
|
||||||
|
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
|
||||||
|
const oauthError = hash.get("error_description") ?? hash.get("error");
|
||||||
|
|
||||||
|
if (oauthError) {
|
||||||
|
cleanOAuthFragment();
|
||||||
|
throw new Error(oauthError.replace(/\+/g, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = hash.get("access_token");
|
||||||
|
const refreshToken = hash.get("refresh_token");
|
||||||
|
if (!accessToken || !refreshToken) return null;
|
||||||
|
|
||||||
|
const expiresAt = resolveExpiry(hash.get("expires_at"), hash.get("expires_in"));
|
||||||
|
|
||||||
|
// Retira los tokens de la barra de direcciones antes de hacer más solicitudes.
|
||||||
|
cleanOAuthFragment();
|
||||||
|
|
||||||
|
const user = await fetchUser(accessToken);
|
||||||
|
const session = { accessToken, refreshToken, expiresAt, user };
|
||||||
|
|
||||||
|
storeSession(session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFreshSession(session: SupabaseAuthSession): Promise<SupabaseAuthSession> {
|
||||||
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||||
|
if (session.expiresAt > nowSeconds + EXPIRY_MARGIN_SECONDS) return session;
|
||||||
|
|
||||||
|
const response = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: publicHeaders(),
|
||||||
|
body: JSON.stringify({ refresh_token: session.refreshToken }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getResponseError(response, "La sesión expiró."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = (await response.json()) as TokenResponse;
|
||||||
|
const refreshed: SupabaseAuthSession = {
|
||||||
|
accessToken: tokenData.access_token,
|
||||||
|
refreshToken: tokenData.refresh_token,
|
||||||
|
expiresAt: resolveExpiry(tokenData.expires_at, tokenData.expires_in),
|
||||||
|
user: tokenData.user ?? (await fetchUser(tokenData.access_token)),
|
||||||
|
};
|
||||||
|
|
||||||
|
storeSession(refreshed);
|
||||||
|
return refreshed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUser(accessToken: string): Promise<SupabaseAuthUser> {
|
||||||
|
const response = await fetch(`${supabaseUrl}/auth/v1/user`, {
|
||||||
|
headers: authenticatedHeaders(accessToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await getResponseError(response, "No se pudo cargar el usuario."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = (await response.json()) as Partial<SupabaseAuthUser>;
|
||||||
|
if (!user.id || !user.email) {
|
||||||
|
throw new Error("Supabase no devolvió un usuario válido.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return user as SupabaseAuthUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicHeaders(): HeadersInit {
|
||||||
|
return {
|
||||||
|
apikey: supabaseKey,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticatedHeaders(accessToken: string): HeadersInit {
|
||||||
|
return {
|
||||||
|
...publicHeaders(),
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredSession(): SupabaseAuthSession | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(SESSION_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw) as Partial<SupabaseAuthSession>;
|
||||||
|
if (
|
||||||
|
!parsed.accessToken ||
|
||||||
|
!parsed.refreshToken ||
|
||||||
|
!parsed.expiresAt ||
|
||||||
|
!parsed.user?.id ||
|
||||||
|
!parsed.user.email
|
||||||
|
) {
|
||||||
|
clearStoredSession();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed as SupabaseAuthSession;
|
||||||
|
} catch {
|
||||||
|
clearStoredSession();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeSession(session: SupabaseAuthSession): void {
|
||||||
|
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStoredSession(): void {
|
||||||
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanOAuthFragment(): void {
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
document.title,
|
||||||
|
`${window.location.pathname}${window.location.search}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExpiry(
|
||||||
|
expiresAt: string | number | null | undefined,
|
||||||
|
expiresIn: string | number | null | undefined,
|
||||||
|
): number {
|
||||||
|
const absolute = Number(expiresAt);
|
||||||
|
if (Number.isFinite(absolute) && absolute > 0) return absolute;
|
||||||
|
|
||||||
|
const duration = Number(expiresIn);
|
||||||
|
return Math.floor(Date.now() / 1000) + (Number.isFinite(duration) ? duration : 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResponseError(response: Response, fallback: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
message?: string;
|
||||||
|
msg?: string;
|
||||||
|
error?: string;
|
||||||
|
error_description?: string;
|
||||||
|
};
|
||||||
|
return payload.message ?? payload.msg ?? payload.error_description ?? payload.error ?? fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertConfigured(): void {
|
||||||
|
const message = getSupabaseConfigurationMessage();
|
||||||
|
if (message) throw new Error(message);
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import type { SupabaseAuthSession } from "./supabase-auth";
|
||||||
|
import type { Contact, ContactArea, Country, TaxCategory, TaxEvent } from "./tax-utils";
|
||||||
|
|
||||||
|
const supabaseUrl = String(import.meta.env.VITE_SUPABASE_URL ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
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();
|
||||||
|
|
||||||
|
type CountryRow = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
is_regional: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ObligationRow = {
|
||||||
|
id: string;
|
||||||
|
due_date: string;
|
||||||
|
description: string;
|
||||||
|
country_id: string;
|
||||||
|
category: TaxCategory;
|
||||||
|
source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeliveryStatusRow = {
|
||||||
|
obligation_id: string;
|
||||||
|
has_sent_notification: boolean;
|
||||||
|
first_sent_at: string | null;
|
||||||
|
sent_notification_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ObligationDeliveryStatus = {
|
||||||
|
obligationId: string;
|
||||||
|
hasSentNotification: boolean;
|
||||||
|
firstSentAt: string | null;
|
||||||
|
sentNotificationCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContactRow = {
|
||||||
|
id: string;
|
||||||
|
country_id: string;
|
||||||
|
full_name: string;
|
||||||
|
email: string | null;
|
||||||
|
whatsapp_number: string | null;
|
||||||
|
job_title: string | null;
|
||||||
|
area: ContactArea;
|
||||||
|
email_enabled: boolean;
|
||||||
|
whatsapp_enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewObligation = {
|
||||||
|
date: string;
|
||||||
|
text: string;
|
||||||
|
countryId: string;
|
||||||
|
category: TaxCategory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewContact = {
|
||||||
|
countryId: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
area: ContactArea;
|
||||||
|
emailEnabled: boolean;
|
||||||
|
whatsappEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadObligationDeliveryStatuses(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
): Promise<ObligationDeliveryStatus[]> {
|
||||||
|
const rows = await request<DeliveryStatusRow[]>(
|
||||||
|
session,
|
||||||
|
"/rest/v1/rpc/tax_obligation_delivery_status",
|
||||||
|
{ method: "POST", body: "{}" },
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
obligationId: row.obligation_id,
|
||||||
|
hasSentNotification: row.has_sent_notification,
|
||||||
|
firstSentAt: row.first_sent_at,
|
||||||
|
sentNotificationCount: Number(row.sent_notification_count ?? 0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTaxData(session: SupabaseAuthSession): Promise<{
|
||||||
|
countries: Country[];
|
||||||
|
events: TaxEvent[];
|
||||||
|
contacts: Contact[];
|
||||||
|
}> {
|
||||||
|
const countries = await getCountries(session);
|
||||||
|
const countryMap = new Map(countries.map((country) => [country.id, country]));
|
||||||
|
|
||||||
|
const [eventRows, contactRows, deliveryRows] = await Promise.all([
|
||||||
|
request<ObligationRow[]>(
|
||||||
|
session,
|
||||||
|
"/rest/v1/tax_obligations?select=id,due_date,description,country_id,category,source&active=eq.true&order=due_date.asc,description.asc",
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
loadObligationDeliveryStatuses(session),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deliveryMap = new Map(deliveryRows.map((row) => [row.obligationId, row]));
|
||||||
|
|
||||||
|
const events = eventRows.map((row) => {
|
||||||
|
const country = countryMap.get(row.country_id);
|
||||||
|
if (!country) throw new Error("Una obligación tiene un país inválido.");
|
||||||
|
const delivery = deliveryMap.get(row.id);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
date: row.due_date,
|
||||||
|
text: row.description,
|
||||||
|
countryId: row.country_id,
|
||||||
|
country: country.name,
|
||||||
|
countryCode: country.code,
|
||||||
|
category: row.category,
|
||||||
|
source: row.source,
|
||||||
|
hasSentNotification: delivery?.hasSentNotification ?? false,
|
||||||
|
firstSentAt: delivery?.firstSentAt ?? null,
|
||||||
|
sentNotificationCount: delivery?.sentNotificationCount ?? 0,
|
||||||
|
} 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { countries, events, contacts };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createObligation(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
input: NewObligation,
|
||||||
|
): Promise<TaxEvent> {
|
||||||
|
const [row] = await request<ObligationRow[]>(session, "/rest/v1/tax_obligations", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
due_date: input.date,
|
||||||
|
description: input.text.trim(),
|
||||||
|
country_id: input.countryId,
|
||||||
|
category: input.category,
|
||||||
|
source: "app",
|
||||||
|
is_historical: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 de la obligación.");
|
||||||
|
|
||||||
|
void notifyWorkflow("obligation.created", {
|
||||||
|
obligation_id: row.id,
|
||||||
|
due_date: row.due_date,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
date: row.due_date,
|
||||||
|
text: row.description,
|
||||||
|
countryId: row.country_id,
|
||||||
|
country: country.name,
|
||||||
|
countryCode: country.code,
|
||||||
|
category: row.category,
|
||||||
|
source: row.source,
|
||||||
|
hasSentNotification: false,
|
||||||
|
firstSentAt: null,
|
||||||
|
sentNotificationCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateObligation(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
id: string,
|
||||||
|
input: NewObligation,
|
||||||
|
): Promise<TaxEvent> {
|
||||||
|
const [row] = await request<ObligationRow[]>(
|
||||||
|
session,
|
||||||
|
`/rest/v1/tax_obligations?id=eq.${encodeURIComponent(id)}`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
due_date: input.date,
|
||||||
|
description: input.text.trim(),
|
||||||
|
country_id: input.countryId,
|
||||||
|
category: input.category,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) throw new Error("No se encontró la obligación.");
|
||||||
|
|
||||||
|
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 de la obligación.");
|
||||||
|
|
||||||
|
void notifyWorkflow("obligation.updated", {
|
||||||
|
obligation_id: row.id,
|
||||||
|
due_date: row.due_date,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
date: row.due_date,
|
||||||
|
text: row.description,
|
||||||
|
countryId: row.country_id,
|
||||||
|
country: country.name,
|
||||||
|
countryCode: country.code,
|
||||||
|
category: row.category,
|
||||||
|
source: row.source,
|
||||||
|
hasSentNotification: false,
|
||||||
|
firstSentAt: null,
|
||||||
|
sentNotificationCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeObligation(session: SupabaseAuthSession, id: string): Promise<void> {
|
||||||
|
await request(session, `/rest/v1/tax_obligations?id=eq.${encodeURIComponent(id)}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { Prefer: "return=minimal" },
|
||||||
|
body: JSON.stringify({ active: false }),
|
||||||
|
});
|
||||||
|
void notifyWorkflow("obligation.deleted", { obligation_id: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createContact(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
input: NewContact,
|
||||||
|
): Promise<Contact> {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
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.");
|
||||||
|
|
||||||
|
void notifyWorkflow("contact.created", {
|
||||||
|
contact_id: row.id,
|
||||||
|
country_id: row.country_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 removeContact(session: SupabaseAuthSession, id: string): Promise<void> {
|
||||||
|
await request(session, `/rest/v1/tax_contacts?id=eq.${encodeURIComponent(id)}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { Prefer: "return=minimal" },
|
||||||
|
body: JSON.stringify({ active: false }),
|
||||||
|
});
|
||||||
|
void notifyWorkflow("contact.deleted", { contact_id: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCountries(session: SupabaseAuthSession): Promise<Country[]> {
|
||||||
|
const rows = await request<CountryRow[]>(
|
||||||
|
session,
|
||||||
|
"/rest/v1/tax_countries?select=id,code,name,is_regional,sort_order&active=eq.true&order=sort_order.asc,name.asc",
|
||||||
|
);
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
code: row.code,
|
||||||
|
name: row.name,
|
||||||
|
isRegional: row.is_regional,
|
||||||
|
sortOrder: row.sort_order,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T = unknown>(
|
||||||
|
session: SupabaseAuthSession,
|
||||||
|
path: string,
|
||||||
|
init: RequestInit = {},
|
||||||
|
): Promise<T> {
|
||||||
|
const headers = new Headers(init.headers);
|
||||||
|
headers.set("apikey", supabaseKey);
|
||||||
|
headers.set("Authorization", `Bearer ${session.accessToken}`);
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
|
||||||
|
const response = await fetch(`${supabaseUrl}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await readError(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) return undefined as T;
|
||||||
|
const text = await response.text();
|
||||||
|
return (text ? JSON.parse(text) : undefined) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readError(response: Response): Promise<string> {
|
||||||
|
try {
|
||||||
|
const payload = (await response.json()) as { message?: string; details?: string };
|
||||||
|
return payload.message ?? payload.details ?? "No se pudo guardar el cambio.";
|
||||||
|
} catch {
|
||||||
|
return "No se pudo guardar el cambio.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 notifyWorkflow(action: string, payload: Record<string, unknown>) {
|
||||||
|
if (!workflowUrl) return;
|
||||||
|
try {
|
||||||
|
await fetch(workflowUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
export type TaxCategory = "nomina" | "admin";
|
||||||
|
|
||||||
|
export type Country = {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
isRegional: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaxEvent = {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
text: string;
|
||||||
|
countryId: string;
|
||||||
|
country: string;
|
||||||
|
countryCode: string;
|
||||||
|
category: TaxCategory;
|
||||||
|
source?: string;
|
||||||
|
hasSentNotification: boolean;
|
||||||
|
firstSentAt: string | null;
|
||||||
|
sentNotificationCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContactArea = "admin" | "nomina" | "regional";
|
||||||
|
|
||||||
|
export type Contact = {
|
||||||
|
id: string;
|
||||||
|
countryId: string;
|
||||||
|
country: string;
|
||||||
|
countryCode: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
area: ContactArea;
|
||||||
|
emailEnabled: boolean;
|
||||||
|
whatsappEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Siempre se alerta el miércoles anterior (estrictamente anterior) y la fecha
|
||||||
|
// límite. Esto incluye obligaciones que vencen sábados y domingos.
|
||||||
|
export function getAlertDates(dueISO: string): string[] {
|
||||||
|
const [year, month, day] = dueISO.split("-").map(Number);
|
||||||
|
const due = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
const dow = due.getUTCDay(); // 0=domingo ... 6=sábado
|
||||||
|
const daysSinceWednesday = (dow - 3 + 7) % 7;
|
||||||
|
const strictPreviousWednesday = daysSinceWednesday === 0 ? 7 : daysSinceWednesday;
|
||||||
|
|
||||||
|
const previousWednesday = new Date(due);
|
||||||
|
previousWednesday.setUTCDate(due.getUTCDate() - strictPreviousWednesday);
|
||||||
|
|
||||||
|
return [toISO(previousWednesday), dueISO].filter(
|
||||||
|
(value, index, all) => all.indexOf(value) === index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toISO(date: Date): string {
|
||||||
|
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MONTHS_ES = [
|
||||||
|
"Enero",
|
||||||
|
"Febrero",
|
||||||
|
"Marzo",
|
||||||
|
"Abril",
|
||||||
|
"Mayo",
|
||||||
|
"Junio",
|
||||||
|
"Julio",
|
||||||
|
"Agosto",
|
||||||
|
"Septiembre",
|
||||||
|
"Octubre",
|
||||||
|
"Noviembre",
|
||||||
|
"Diciembre",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DAYS_ES = ["Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Dom"];
|
||||||
|
|
||||||
|
export function formatDate(iso: string): string {
|
||||||
|
const [year, month, day] = iso.split("-").map(Number);
|
||||||
|
return `${day} ${MONTHS_ES[month - 1]} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSearch(value: string): string {
|
||||||
|
return value
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("root");
|
||||||
|
|
||||||
|
if (!rootElement) {
|
||||||
|
throw new Error('No se encontró el elemento raíz con id="root".');
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(rootElement).render(<App />);
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
@import "tailwindcss" source(none);
|
||||||
|
@source "../src";
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
--color-glm-blue: var(--glm-blue);
|
||||||
|
--color-glm-blue-soft: var(--glm-blue-soft);
|
||||||
|
--color-glm-green: var(--glm-green);
|
||||||
|
--color-glm-green-light: var(--glm-green-light);
|
||||||
|
--color-glm-lime: var(--glm-lime);
|
||||||
|
--color-glm-orange: var(--glm-orange);
|
||||||
|
--color-glm-teal: var(--glm-teal);
|
||||||
|
--color-nomina: var(--glm-orange);
|
||||||
|
--color-admin: var(--glm-blue);
|
||||||
|
|
||||||
|
--font-sans: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.75rem;
|
||||||
|
|
||||||
|
/* GLM brand */
|
||||||
|
--glm-blue: oklch(0.55 0.045 235); /* #4F758B */
|
||||||
|
--glm-blue-soft: oklch(0.94 0.015 235);
|
||||||
|
--glm-green: oklch(0.78 0.18 138); /* #6CC24A */
|
||||||
|
--glm-green-light: oklch(0.85 0.16 130); /* #A4D65E */
|
||||||
|
--glm-lime: oklch(0.88 0.19 118); /* #C4D600 */
|
||||||
|
--glm-orange: oklch(0.68 0.21 40); /* #FF6A13 */
|
||||||
|
--glm-teal: oklch(0.48 0.07 195); /* #2C6E6F */
|
||||||
|
|
||||||
|
/* semantic */
|
||||||
|
--background: oklch(0.99 0.005 230);
|
||||||
|
--foreground: oklch(0.22 0.03 235);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.22 0.03 235);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.22 0.03 235);
|
||||||
|
--primary: var(--glm-blue);
|
||||||
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
|
--secondary: oklch(0.96 0.01 230);
|
||||||
|
--secondary-foreground: var(--glm-blue);
|
||||||
|
--muted: oklch(0.96 0.008 230);
|
||||||
|
--muted-foreground: oklch(0.5 0.02 235);
|
||||||
|
--accent: var(--glm-green);
|
||||||
|
--accent-foreground: oklch(0.99 0 0);
|
||||||
|
--destructive: oklch(0.62 0.22 25);
|
||||||
|
--destructive-foreground: oklch(0.99 0 0);
|
||||||
|
--border: oklch(0.9 0.012 230);
|
||||||
|
--input: oklch(0.92 0.01 230);
|
||||||
|
--ring: var(--glm-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-grid {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
color-mix(in oklch, var(--glm-blue) 7%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
color-mix(in oklch, var(--glm-blue) 7%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
background-size: 42px 42px;
|
||||||
|
mask-image: radial-gradient(circle at center, black 15%, transparent 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the application inside the browser viewport without a page scrollbar. */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content remains usable on shorter screens, but no visual scrollbar is shown. */
|
||||||
|
.app-scrollbar-hidden {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-scrollbar-hidden::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_SUPABASE_URL?: string;
|
||||||
|
readonly VITE_SUPABASE_PUBLISHABLE_KEY?: string;
|
||||||
|
readonly VITE_SUPABASE_ANON_KEY?: string;
|
||||||
|
readonly VITE_TAX_WEBHOOK_URL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Configuración de Supabase
|
||||||
|
|
||||||
|
Ejecuta una sola vez el script completo:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```sql
|
||||||
|
insert into public.tax_calendar_access (email, full_name)
|
||||||
|
values ('nuevo@gomezleemarketing.com', 'Nombre Apellido')
|
||||||
|
on conflict (email) do update
|
||||||
|
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.
|
||||||
@@ -0,0 +1,932 @@
|
|||||||
|
-- Seguimiento de Impuestos GLM
|
||||||
|
-- Esquema completo, seguridad, datos históricos 2026, contactos y funciones para n8n.
|
||||||
|
-- Ejecutar una sola vez en el SQL Editor de Supabase.
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Funciones comunes
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create or replace function public.tax_set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at := now();
|
||||||
|
if auth.uid() is not null then
|
||||||
|
new.updated_by := auth.uid();
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Control de acceso a la app
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_calendar_access (
|
||||||
|
email text primary key,
|
||||||
|
full_name text,
|
||||||
|
active boolean not null default true,
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_calendar_access_email_not_blank check (length(trim(email)) > 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
create or replace function public.tax_normalize_access_email()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.email := lower(trim(new.email));
|
||||||
|
new.updated_at := now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_normalize_access_email_trigger on public.tax_calendar_access;
|
||||||
|
create trigger tax_normalize_access_email_trigger
|
||||||
|
before insert or update on public.tax_calendar_access
|
||||||
|
for each row execute function public.tax_normalize_access_email();
|
||||||
|
|
||||||
|
create or replace function public.has_tax_calendar_access()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_calendar_access access
|
||||||
|
where access.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and access.active = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.has_tax_calendar_access() from public, anon;
|
||||||
|
grant execute on function public.has_tax_calendar_access() to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Catálogo de países
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_countries (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
code text not null unique,
|
||||||
|
name text not null unique,
|
||||||
|
is_regional boolean not null default false,
|
||||||
|
sort_order integer not null default 100,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
updated_by uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into public.tax_countries (code, name, is_regional, sort_order)
|
||||||
|
values
|
||||||
|
('REG', 'Regional', true, 0),
|
||||||
|
('CO', 'Colombia', false, 10),
|
||||||
|
('CR', 'Costa Rica', false, 20),
|
||||||
|
('SV', 'El Salvador', false, 30),
|
||||||
|
('GT', 'Guatemala', false, 40),
|
||||||
|
('HN', 'Honduras', false, 50),
|
||||||
|
('JM', 'Jamaica', false, 60),
|
||||||
|
('MX', 'México', false, 70),
|
||||||
|
('NI', 'Nicaragua', false, 80),
|
||||||
|
('PA', 'Panamá', false, 90),
|
||||||
|
('PR', 'Puerto Rico', false, 100),
|
||||||
|
('DO', 'República Dominicana', false, 110),
|
||||||
|
('TT', 'Trinidad y Tobago', false, 120)
|
||||||
|
on conflict (code) do update
|
||||||
|
set name = excluded.name,
|
||||||
|
is_regional = excluded.is_regional,
|
||||||
|
sort_order = excluded.sort_order,
|
||||||
|
active = true,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
drop trigger if exists tax_countries_updated_at_trigger on public.tax_countries;
|
||||||
|
create trigger tax_countries_updated_at_trigger
|
||||||
|
before update on public.tax_countries
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Obligaciones tributarias
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_obligations (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
due_date date not null,
|
||||||
|
description text not null,
|
||||||
|
category text not null check (category in ('nomina', 'admin')),
|
||||||
|
country_id uuid not null references public.tax_countries(id),
|
||||||
|
source text not null default 'app',
|
||||||
|
is_historical boolean not null default false,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_by uuid default auth.uid(),
|
||||||
|
updated_by uuid default auth.uid(),
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_obligations_description_not_blank check (length(trim(description)) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index if not exists tax_obligations_unique_active_event
|
||||||
|
on public.tax_obligations (due_date, country_id, lower(description))
|
||||||
|
where active = true;
|
||||||
|
|
||||||
|
drop trigger if exists tax_obligations_updated_at_trigger on public.tax_obligations;
|
||||||
|
create trigger tax_obligations_updated_at_trigger
|
||||||
|
before update on public.tax_obligations
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Contactos y canales
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_contacts (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
country_id uuid not null references public.tax_countries(id),
|
||||||
|
full_name text not null,
|
||||||
|
email text,
|
||||||
|
whatsapp_number text,
|
||||||
|
job_title text,
|
||||||
|
area text not null check (area in ('admin', 'nomina', 'regional')),
|
||||||
|
email_enabled boolean not null default true,
|
||||||
|
whatsapp_enabled boolean not null default true,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_by uuid default auth.uid(),
|
||||||
|
updated_by uuid default auth.uid(),
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_contacts_name_not_blank check (length(trim(full_name)) > 0),
|
||||||
|
constraint tax_contacts_email_when_enabled check (not email_enabled or length(trim(coalesce(email, ''))) > 3),
|
||||||
|
constraint tax_contacts_whatsapp_when_enabled check (not whatsapp_enabled or length(trim(coalesce(whatsapp_number, ''))) >= 8)
|
||||||
|
);
|
||||||
|
|
||||||
|
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, '')), '');
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_contacts_normalize_trigger on public.tax_contacts;
|
||||||
|
create trigger tax_contacts_normalize_trigger
|
||||||
|
before insert or update on public.tax_contacts
|
||||||
|
for each row execute function public.tax_normalize_contact();
|
||||||
|
|
||||||
|
create unique index if not exists tax_contacts_unique_active_email
|
||||||
|
on public.tax_contacts (country_id, lower(email))
|
||||||
|
where active = true and email is not null;
|
||||||
|
|
||||||
|
drop trigger if exists tax_contacts_updated_at_trigger on public.tax_contacts;
|
||||||
|
create trigger tax_contacts_updated_at_trigger
|
||||||
|
before update on public.tax_contacts
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Bitácora de envíos: evita recordatorios duplicados
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_notification_log (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
obligation_id uuid not null references public.tax_obligations(id),
|
||||||
|
contact_id uuid not null references public.tax_contacts(id),
|
||||||
|
alert_date date not null,
|
||||||
|
channel text not null check (channel in ('email', 'whatsapp')),
|
||||||
|
status text not null default 'sent' check (status in ('sent', 'failed')),
|
||||||
|
provider_response jsonb,
|
||||||
|
sent_at timestamptz not null default now(),
|
||||||
|
unique (obligation_id, contact_id, alert_date, channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Estado de envío y bloqueo de obligaciones notificadas
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create or replace function public.tax_obligation_delivery_status()
|
||||||
|
returns table (
|
||||||
|
obligation_id uuid,
|
||||||
|
has_sent_notification boolean,
|
||||||
|
first_sent_at timestamptz,
|
||||||
|
sent_notification_count bigint
|
||||||
|
)
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select
|
||||||
|
obligation.id as obligation_id,
|
||||||
|
count(log.id) filter (where log.status = 'sent') > 0 as has_sent_notification,
|
||||||
|
min(log.sent_at) filter (where log.status = 'sent') as first_sent_at,
|
||||||
|
count(log.id) filter (where log.status = 'sent') as sent_notification_count
|
||||||
|
from public.tax_obligations obligation
|
||||||
|
left join public.tax_notification_log log
|
||||||
|
on log.obligation_id = obligation.id
|
||||||
|
where obligation.active = true
|
||||||
|
and public.has_tax_calendar_access()
|
||||||
|
group by obligation.id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.tax_obligation_delivery_status()
|
||||||
|
from public, anon;
|
||||||
|
grant execute on function public.tax_obligation_delivery_status()
|
||||||
|
to authenticated;
|
||||||
|
|
||||||
|
create or replace function public.tax_prevent_sent_obligation_changes()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_notification_log log
|
||||||
|
where log.obligation_id = old.id
|
||||||
|
and log.status = 'sent'
|
||||||
|
) and (
|
||||||
|
new.due_date is distinct from old.due_date
|
||||||
|
or new.description is distinct from old.description
|
||||||
|
or new.category is distinct from old.category
|
||||||
|
or new.country_id is distinct from old.country_id
|
||||||
|
or new.active is distinct from old.active
|
||||||
|
) then
|
||||||
|
raise exception using
|
||||||
|
errcode = 'P0001',
|
||||||
|
message = 'Esta obligación ya generó avisos y no puede modificarse ni eliminarse.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_obligations_lock_after_sent_trigger
|
||||||
|
on public.tax_obligations;
|
||||||
|
|
||||||
|
create trigger tax_obligations_lock_after_sent_trigger
|
||||||
|
before update on public.tax_obligations
|
||||||
|
for each row execute function public.tax_prevent_sent_obligation_changes();
|
||||||
|
|
||||||
|
-- Miércoles estrictamente anterior a la fecha límite.
|
||||||
|
create or replace function public.tax_previous_wednesday(p_due_date date)
|
||||||
|
returns date
|
||||||
|
language sql
|
||||||
|
immutable
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select p_due_date -
|
||||||
|
case
|
||||||
|
when mod((extract(dow from p_due_date)::integer - 3 + 7), 7) = 0 then 7
|
||||||
|
else mod((extract(dow from p_due_date)::integer - 3 + 7), 7)
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
comment on function public.tax_previous_wednesday(date) is
|
||||||
|
'Calcula el miércoles estrictamente anterior a una fecha límite.';
|
||||||
|
|
||||||
|
-- Devuelve una fila por obligación y destinatario pendiente de notificar.
|
||||||
|
create or replace 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,
|
||||||
|
contact_whatsapp text,
|
||||||
|
email_enabled boolean,
|
||||||
|
whatsapp_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.whatsapp_number as contact_whatsapp,
|
||||||
|
(
|
||||||
|
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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
and log.status = 'sent'
|
||||||
|
)
|
||||||
|
) as whatsapp_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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
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;
|
||||||
|
|
||||||
|
create or replace function public.tax_record_notification(
|
||||||
|
p_obligation_id uuid,
|
||||||
|
p_contact_id uuid,
|
||||||
|
p_alert_date date,
|
||||||
|
p_channel text,
|
||||||
|
p_status text default 'sent',
|
||||||
|
p_provider_response jsonb default null
|
||||||
|
)
|
||||||
|
returns void
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.tax_notification_log (
|
||||||
|
obligation_id,
|
||||||
|
contact_id,
|
||||||
|
alert_date,
|
||||||
|
channel,
|
||||||
|
status,
|
||||||
|
provider_response,
|
||||||
|
sent_at
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
p_obligation_id,
|
||||||
|
p_contact_id,
|
||||||
|
p_alert_date,
|
||||||
|
p_channel,
|
||||||
|
p_status,
|
||||||
|
p_provider_response,
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
on conflict (obligation_id, contact_id, alert_date, channel)
|
||||||
|
do update set
|
||||||
|
status = excluded.status,
|
||||||
|
provider_response = excluded.provider_response,
|
||||||
|
sent_at = now();
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.tax_record_notification(uuid, uuid, date, text, text, jsonb)
|
||||||
|
from public, anon, authenticated;
|
||||||
|
grant execute on function public.tax_record_notification(uuid, uuid, date, text, text, jsonb)
|
||||||
|
to service_role;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Seguridad RLS para el frontend
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
alter table public.tax_calendar_access enable row level security;
|
||||||
|
alter table public.tax_countries enable row level security;
|
||||||
|
alter table public.tax_obligations enable row level security;
|
||||||
|
alter table public.tax_contacts enable row level security;
|
||||||
|
alter table public.tax_notification_log enable row level security;
|
||||||
|
|
||||||
|
revoke all on table public.tax_calendar_access from anon, authenticated;
|
||||||
|
revoke all on table public.tax_notification_log from anon, authenticated;
|
||||||
|
|
||||||
|
grant select on table public.tax_countries to authenticated;
|
||||||
|
grant select, insert, update on table public.tax_obligations to authenticated;
|
||||||
|
grant select, insert, update on table public.tax_contacts to authenticated;
|
||||||
|
|
||||||
|
drop policy if exists tax_countries_authorized_read on public.tax_countries;
|
||||||
|
create policy tax_countries_authorized_read
|
||||||
|
on public.tax_countries for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_read on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_read
|
||||||
|
on public.tax_obligations for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_insert on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_insert
|
||||||
|
on public.tax_obligations for insert
|
||||||
|
to authenticated
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_update on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_update
|
||||||
|
on public.tax_obligations for update
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access())
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_read on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_read
|
||||||
|
on public.tax_contacts for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_insert on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_insert
|
||||||
|
on public.tax_contacts for insert
|
||||||
|
to authenticated
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_update on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_update
|
||||||
|
on public.tax_contacts for update
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access())
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Usuarios autorizados iniciales
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
insert into public.tax_calendar_access (email)
|
||||||
|
values
|
||||||
|
('ethen@gomezleemarketing.com'),
|
||||||
|
('iaracena@gomezleemarketing.com'),
|
||||||
|
('lmatos@gomezleemarketing.com'),
|
||||||
|
('asrodriguez@gomezleemarketing.com')
|
||||||
|
on conflict (email) do update
|
||||||
|
set active = true,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Contactos iniciales
|
||||||
|
-- Los números de WhatsApp quedan en formato internacional, sin el signo +.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
with contact_data(country_name, full_name, email, whatsapp_number, job_title, area) as (
|
||||||
|
values
|
||||||
|
('Guatemala', 'Narcisa Yessenia Barrera Godoy', 'administrativoguatemala2@gomezleemarketing.com', '50255581600', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('República Dominicana', 'Erika Benitez', 'administrativord@gomezleemarketing.com', '18099662004', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Honduras', 'Bessy Calix Pineda', 'administrativohonduras@gomezleemarketing.com', '50495890865', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Colombia', 'Natalia Casas Avella', 'administrativocolombia@gomezleemarketing.com', '573137702391', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Costa Rica', 'Fernanda Céspedes Vargas', 'admincr@gomezleemarketing.com', '50686236444', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Angie Chavez Silva', 'administrativonicaragua@gomezleemarketing.com', '50585263292', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Panamá', 'Jheysana Garaban Quintero', 'administrativopty2@gomezleemarketing.com', '50764548936', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('República Dominicana', 'Adrianita Gonzalez Figueroa', 'agonzalez@gomezleemarketing.com', '18094328347', 'Aux. Administrativo', 'admin'),
|
||||||
|
('El Salvador', 'Noemi Elizabeth Mena Medina', 'nmena02@gomezleemarketing.com', '50378102952', 'Aux. Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Estefani Orozco Davila', 'administrativo@gomezleemarketing.com', '50585306316', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Panamá', 'Carlos Paredes Garcia', 'cparedes@gomezleemarketing.com', '5073975475', 'Asistente Administrativo', 'admin'),
|
||||||
|
('Colombia', 'Lady Perdomo Suarez', 'administrativocolombia2@gomezleemarketing.com', '573222062462', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Guatemala', 'Carmen Estela Sierra Arias', 'administrativoguatemala@gomezleemarketing.com', '50258469211', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Guatemala', 'Shirley Velásquez González', 'administrativoguatemala3@gomezleemarketing.com', '50237727117', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Seyling Lissamary Zambrana Reyes', 'auxiliaradmonnic@gomezleemarketing.com', '50584241616', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Regional', 'Ada Rodríguez', 'asrodriguez@gomezleemarketing.com', '50233335008', 'Coordinador Regional Administrativo', 'regional'),
|
||||||
|
('Regional', 'Zoeya Salomon Imbert', 'zsalomon@gomezleemarketing.com', '18293874206', 'Coordinador Regional Administrativo', 'regional'),
|
||||||
|
('Regional', 'Brayan Alexander Licona Oseguera', 'conciliaciones@gomezleemarketing.com', '50432077484', 'Coordinador Administrativo', 'regional'),
|
||||||
|
('Regional', 'Viviana Páramo Gonzalez', 'vparamo@gomezleemarketing.com', '573105474049', 'Subgerente Administrativo Regional', 'regional'),
|
||||||
|
('Regional', 'Mati Soto Valenzuela', 'msoto@gomezleemarketing.com', '18094037571', 'Nómina Regional', 'nomina'),
|
||||||
|
('Regional', 'Iveth Herrera', 'iherrera@gomezleemarketing.com', '50376373732', 'Nómina Regional', 'nomina')
|
||||||
|
)
|
||||||
|
insert into public.tax_contacts (
|
||||||
|
country_id,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
whatsapp_number,
|
||||||
|
job_title,
|
||||||
|
area,
|
||||||
|
email_enabled,
|
||||||
|
whatsapp_enabled
|
||||||
|
)
|
||||||
|
select
|
||||||
|
country.id,
|
||||||
|
data.full_name,
|
||||||
|
lower(data.email),
|
||||||
|
data.whatsapp_number,
|
||||||
|
data.job_title,
|
||||||
|
data.area,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
from contact_data data
|
||||||
|
join public.tax_countries country on country.name = data.country_name
|
||||||
|
on conflict do nothing;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Migración de las 294 obligaciones históricas del calendario 2026
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
with obligation_data(due_date, description, category, country_name) as (
|
||||||
|
values
|
||||||
|
('2026-01-02'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-01-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-01-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-01-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-01-12'::date, 'Puerto Rico: IVU Impuestos sobre ventas y uso)', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-01-14'::date, 'Panamá: IVA / Municipio', 'admin', 'Panamá'),
|
||||||
|
('2026-01-14'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-01-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-01-14'::date, 'HN: INFOP', 'nomina', 'Honduras'),
|
||||||
|
('2026-01-14'::date, 'NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-15'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-01-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-01-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-01-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-01-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-01-21'::date, 'COL: Retencion en la fuente', 'admin', 'Colombia'),
|
||||||
|
('2026-01-21'::date, 'COL: IVA', 'admin', 'Colombia'),
|
||||||
|
('2026-01-21'::date, 'HN: IHSS y RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-01-22'::date, 'Nicaragua Paga Matricula 2024', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-01-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-01-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-01-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-01-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales; ISO (Anual)', 'admin', 'Guatemala'),
|
||||||
|
('2026-01-30'::date, 'Puerto Rico: Formularios Anuales (W-2, 1099, Reconciliaciones patronales)', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-02-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-02-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-02-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-02-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-02-12'::date, 'Panamá: IVA / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-02-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-02-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-02-13'::date, '- Guatemala: Impuestos de Retenciones /', 'admin', 'Guatemala'),
|
||||||
|
('2026-02-13'::date, '- NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-02-19'::date, 'COL: Retencion fuente (Ene)', 'admin', 'Colombia'),
|
||||||
|
('2026-02-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-02-20'::date, 'COL: ICA BIM 6', 'admin', 'Colombia'),
|
||||||
|
('2026-02-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-02-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-02-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-02-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-02-26'::date, 'COL: ICA Anual 2024', 'admin', 'Colombia'),
|
||||||
|
('2026-02-26'::date, 'Nicaragua-Pago de IR Anual', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-02-27'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-03-02'::date, 'Jamaica: Anual Return and beneficial ownership', 'admin', 'Jamaica'),
|
||||||
|
('2026-03-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-03-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-03-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-03-07'::date, 'Jamaica: Anual Income Tax (1st Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-03-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-03-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-03-12'::date, 'Panamá: IVA / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-03-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-03-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-03-13'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-03-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-03-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-03-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-03-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-03-19'::date, 'COL: Retencion fuente (Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-03-19'::date, 'COL:IVA', 'admin', 'Colombia'),
|
||||||
|
('2026-03-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-03-20'::date, 'COL: Rete Ica Bimestre 1 (Ene-Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-03-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-03-24'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-03-26'::date, 'Costa Rica: RENTA 2023, INS IITrimestre', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-03-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-03-29'::date, 'Panamá: Declaracion de renta', 'admin', 'Panamá'),
|
||||||
|
('2026-04-03'::date, 'COL: ICA 1(Ene-Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-04-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-04-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-04-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Planilla de Contribucion sobre Ingresos', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Quarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-04-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR / NICARAGUA: INSS e INATEC', 'nomina', 'Honduras'),
|
||||||
|
('2026-04-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-04-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-04-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-04-16'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-04-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-04-17'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-18'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-20'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-04-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-04-21'::date, 'COL: Retencion fuente (Marzo) HN: RAP', 'nomina', 'Colombia'),
|
||||||
|
('2026-04-23'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-04-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-04-29'::date, 'Trinidad: Anual Income Tax Return', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-05-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-05-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-05-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-05-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-05-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-05-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC / NICARAGUA: IMPUESTO IMI ALCALDIA', 'nomina', 'Panamá'),
|
||||||
|
('2026-05-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-05-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-05-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-05-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-05-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-05-20'::date, 'COL: Renta Anual (01 cuota)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-20'::date, 'COL: Iva (Ene-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-20'::date, 'COL: Retencion fuente (Abril)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-05-22'::date, 'COL: ReteIca Bimestre 2 (Mar-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-22'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-05-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-06-01'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-06-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-06-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-06-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-06-07'::date, 'Jamaica: Anual Income Tax (2nd Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-06-07'::date, 'Puerto Rico: Qarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-06-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-06-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-06-11'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-06-11'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-06-12'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-06-12'::date, 'COL: ICA 2(Mar-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-06-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-06-12'::date, 'ESV: IVA, Retencion en la fuente, AFP Crecer, AFP Confia', 'nomina', 'El Salvador'),
|
||||||
|
('2026-06-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-06-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-06-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-06-19'::date, 'COL: Retencion fuente (May)', 'admin', 'Colombia'),
|
||||||
|
('2026-06-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-06-22'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-06-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-06-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-06-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-06-29'::date, 'Panamá: Seguridad social / 1ra pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-06-29'::date, 'CostaRica: 1er anticipo de Renta, INS IIIer trimestre', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-06-29'::date, 'TT: Pago Estimada de renta Q1 2025', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-06-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-07-03'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-07-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-07-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-07-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-07-07'::date, 'Puerto Rico: Patente Municipal', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-07-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-07-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-07-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-07-14'::date, 'Panamá: IVA / Municipio / Tasa Unica / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-07-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-07-14'::date, 'ESV: IVA, Retencion en la fuente, AFP Crecer, AFP Confia', 'nomina', 'El Salvador'),
|
||||||
|
('2026-07-16'::date, 'NICARAGUA: INSS e INATECI', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-07-16'::date, 'MX: Impuesto Sobre NóminasIMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-07-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-07-17'::date, 'COL: ReteIca Bimestre 3(May-Jun)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'COL: Retencion fuente (Jun)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'COL: Renta Anual (02 cuota)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'Trinidad: Value Added Tax', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-07-20'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-07-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-07-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-07-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-07-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-07-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-07-29'::date, 'Panamá: Seguridad social / Planilla 03', 'admin', 'Panamá'),
|
||||||
|
('2026-07-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-08-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-08-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-08-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-08-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-08-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-08-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-08-13'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-08-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-08-13'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-08-14'::date, 'COL: ICA 3(May-Jun) / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Colombia'),
|
||||||
|
('2026-08-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-08-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-08-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-08-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-08-21'::date, 'COL: Retencion fuente (Jul) HN: RAP', 'nomina', 'Colombia'),
|
||||||
|
('2026-08-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-08-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-08-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-08-28'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-08-31'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-09-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-09-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-09-07'::date, 'Jamaica: Anual Income Tax (3rd Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-09-07'::date, 'Puerto Rico: Quarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-09-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-09-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-09-10'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-09-11'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-09-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-09-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-09-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-09-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-09-17'::date, 'COL: Iva (May-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-17'::date, 'COL: Retencion Fuente (Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-17'::date, 'Trinidad: Value added Tax', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-09-18'::date, 'COL: ReteIca Bimestre 4(Jul-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-09-21'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-09-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-09-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-09-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-09-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-09-29'::date, 'Panamá: Seguridad social / 2da pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-09-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-29'::date, 'Costa Rica: II anticipo de Renta IVtrimestre del INS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-09-29'::date, 'Trinidad: Quarterly Installment Payments', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-10-02'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-10-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-10-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-10-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-10-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-10-09'::date, 'COL: ICA 4(Jul-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-10-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-10-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-10-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-10-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-10-15'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-10-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-10-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-10-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-10-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-10-20'::date, 'COL: Retencion fuente (Sep)', 'admin', 'Colombia'),
|
||||||
|
('2026-10-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-10-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-10-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-10-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-10-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-10-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-11-05'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-11-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-11-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-11-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-11-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-11-12'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-11-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-11-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-11-13'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-11-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-11-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-11-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-11-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-11-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-20'::date, 'COL: ReteIca Bimestre 5(Sep-Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-23'::date, 'COL: Retencion Fuente (Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-11-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-11-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-11-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-11-30'::date, 'COL: Retencion Fuente (Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-30'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-30'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-12-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-12-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-12-07'::date, 'Jamaica: Anual Income Tax (4th Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-12-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-12-09'::date, 'Honduras: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-12-10'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-12-11'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-11'::date, 'COL: ICA 5(Sep-Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-12-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-12-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-12-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-12-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-12-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-12-18'::date, 'COL: Retencion Fuente (Nov)', 'admin', 'Colombia'),
|
||||||
|
('2026-12-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-12-21'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-12-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-12-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-12-23'::date, 'MX; Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-12-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-12-29'::date, 'Panamá: Seguridad social / 3ra pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-12-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-29'::date, 'Costa Rica: III anticipo de Renta, Ier trimestre 2026 INS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-12-30'::date, 'Trinidad: Quarterly Installment Payment', 'nomina', 'Trinidad y Tobago')
|
||||||
|
)
|
||||||
|
insert into public.tax_obligations (
|
||||||
|
due_date,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
country_id,
|
||||||
|
source,
|
||||||
|
is_historical
|
||||||
|
)
|
||||||
|
select
|
||||||
|
data.due_date,
|
||||||
|
data.description,
|
||||||
|
data.category,
|
||||||
|
country.id,
|
||||||
|
'excel_2026',
|
||||||
|
true
|
||||||
|
from obligation_data data
|
||||||
|
join public.tax_countries country on country.name = data.country_name
|
||||||
|
on conflict do nothing;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Operaciones administrativas frecuentes
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Agregar/reactivar acceso:
|
||||||
|
-- insert into public.tax_calendar_access (email, full_name)
|
||||||
|
-- values ('nuevo@gomezleemarketing.com', 'Nombre Apellido')
|
||||||
|
-- on conflict (email) do update set active = true, full_name = excluded.full_name;
|
||||||
|
|
||||||
|
-- Quitar acceso sin borrar historial:
|
||||||
|
-- update public.tax_calendar_access
|
||||||
|
-- set active = false
|
||||||
|
-- where email = 'persona@gomezleemarketing.com';
|
||||||
|
|
||||||
|
-- Prueba de recordatorios de una fecha:
|
||||||
|
-- select * from public.tax_due_reminders('2026-07-24'::date);
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- Seguimiento de Impuestos GLM
|
||||||
|
-- Actualización para consultar, editar o eliminar eventos desde la app.
|
||||||
|
-- Los eventos quedan bloqueados automáticamente después del primer aviso enviado.
|
||||||
|
-- Ejecutar una sola vez si ya se ejecutó el script completo anteriormente.
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create or replace function public.tax_obligation_delivery_status()
|
||||||
|
returns table (
|
||||||
|
obligation_id uuid,
|
||||||
|
has_sent_notification boolean,
|
||||||
|
first_sent_at timestamptz,
|
||||||
|
sent_notification_count bigint
|
||||||
|
)
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select
|
||||||
|
obligation.id as obligation_id,
|
||||||
|
count(log.id) filter (where log.status = 'sent') > 0 as has_sent_notification,
|
||||||
|
min(log.sent_at) filter (where log.status = 'sent') as first_sent_at,
|
||||||
|
count(log.id) filter (where log.status = 'sent') as sent_notification_count
|
||||||
|
from public.tax_obligations obligation
|
||||||
|
left join public.tax_notification_log log
|
||||||
|
on log.obligation_id = obligation.id
|
||||||
|
where obligation.active = true
|
||||||
|
and public.has_tax_calendar_access()
|
||||||
|
group by obligation.id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.tax_obligation_delivery_status()
|
||||||
|
from public, anon;
|
||||||
|
grant execute on function public.tax_obligation_delivery_status()
|
||||||
|
to authenticated;
|
||||||
|
|
||||||
|
create or replace function public.tax_prevent_sent_obligation_changes()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_notification_log log
|
||||||
|
where log.obligation_id = old.id
|
||||||
|
and log.status = 'sent'
|
||||||
|
) and (
|
||||||
|
new.due_date is distinct from old.due_date
|
||||||
|
or new.description is distinct from old.description
|
||||||
|
or new.category is distinct from old.category
|
||||||
|
or new.country_id is distinct from old.country_id
|
||||||
|
or new.active is distinct from old.active
|
||||||
|
) then
|
||||||
|
raise exception using
|
||||||
|
errcode = 'P0001',
|
||||||
|
message = 'Esta obligación ya generó avisos y no puede modificarse ni eliminarse.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_obligations_lock_after_sent_trigger
|
||||||
|
on public.tax_obligations;
|
||||||
|
|
||||||
|
create trigger tax_obligations_lock_after_sent_trigger
|
||||||
|
before update on public.tax_obligations
|
||||||
|
for each row execute function public.tax_prevent_sent_obligation_changes();
|
||||||
|
|
||||||
|
commit;
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
-- Seguimiento de Impuestos GLM
|
||||||
|
-- Actualización final: contactos regionales de Nómina y enrutamiento global por categoría.
|
||||||
|
-- Ejecutar una sola vez en Supabase SQL Editor sobre la base ya instalada.
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
-- Los contactos ubicados en el país Regional pueden especializarse por categoría:
|
||||||
|
-- area = regional -> recibe todas las obligaciones
|
||||||
|
-- area = nomina -> recibe Nómina de todos los países
|
||||||
|
-- area = admin -> recibe Administración de todos los países
|
||||||
|
create or replace 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,
|
||||||
|
contact_whatsapp text,
|
||||||
|
email_enabled boolean,
|
||||||
|
whatsapp_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.whatsapp_number as contact_whatsapp,
|
||||||
|
(
|
||||||
|
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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
and log.status = 'sent'
|
||||||
|
)
|
||||||
|
) as whatsapp_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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
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;
|
||||||
|
|
||||||
|
-- Actualiza los contactos si ya existen.
|
||||||
|
with regional_country as (
|
||||||
|
select id
|
||||||
|
from public.tax_countries
|
||||||
|
where code = 'REG'
|
||||||
|
limit 1
|
||||||
|
),
|
||||||
|
contact_data(full_name, email, whatsapp_number, job_title) as (
|
||||||
|
values
|
||||||
|
('Mati Soto Valenzuela', 'msoto@gomezleemarketing.com', '18094037571', 'Nómina Regional'),
|
||||||
|
('Iveth Herrera', 'iherrera@gomezleemarketing.com', '50376373732', 'Nómina Regional')
|
||||||
|
)
|
||||||
|
update public.tax_contacts contact
|
||||||
|
set country_id = regional_country.id,
|
||||||
|
full_name = data.full_name,
|
||||||
|
whatsapp_number = data.whatsapp_number,
|
||||||
|
job_title = data.job_title,
|
||||||
|
area = 'nomina',
|
||||||
|
email_enabled = true,
|
||||||
|
whatsapp_enabled = true,
|
||||||
|
active = true,
|
||||||
|
updated_at = now()
|
||||||
|
from contact_data data
|
||||||
|
cross join regional_country
|
||||||
|
where lower(contact.email) = lower(data.email);
|
||||||
|
|
||||||
|
-- Inserta los contactos que todavía no existen.
|
||||||
|
with regional_country as (
|
||||||
|
select id
|
||||||
|
from public.tax_countries
|
||||||
|
where code = 'REG'
|
||||||
|
limit 1
|
||||||
|
),
|
||||||
|
contact_data(full_name, email, whatsapp_number, job_title) as (
|
||||||
|
values
|
||||||
|
('Mati Soto Valenzuela', 'msoto@gomezleemarketing.com', '18094037571', 'Nómina Regional'),
|
||||||
|
('Iveth Herrera', 'iherrera@gomezleemarketing.com', '50376373732', 'Nómina Regional')
|
||||||
|
)
|
||||||
|
insert into public.tax_contacts (
|
||||||
|
country_id,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
whatsapp_number,
|
||||||
|
job_title,
|
||||||
|
area,
|
||||||
|
email_enabled,
|
||||||
|
whatsapp_enabled,
|
||||||
|
active
|
||||||
|
)
|
||||||
|
select
|
||||||
|
regional_country.id,
|
||||||
|
data.full_name,
|
||||||
|
lower(data.email),
|
||||||
|
data.whatsapp_number,
|
||||||
|
data.job_title,
|
||||||
|
'nomina',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
from contact_data data
|
||||||
|
cross join regional_country
|
||||||
|
where not exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_contacts existing
|
||||||
|
where lower(existing.email) = lower(data.email)
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
||||||
@@ -0,0 +1,932 @@
|
|||||||
|
-- Seguimiento de Impuestos GLM
|
||||||
|
-- Esquema completo, seguridad, datos históricos 2026, contactos y funciones para n8n.
|
||||||
|
-- Ejecutar una sola vez en el SQL Editor de Supabase.
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Funciones comunes
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create or replace function public.tax_set_updated_at()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.updated_at := now();
|
||||||
|
if auth.uid() is not null then
|
||||||
|
new.updated_by := auth.uid();
|
||||||
|
end if;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Control de acceso a la app
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_calendar_access (
|
||||||
|
email text primary key,
|
||||||
|
full_name text,
|
||||||
|
active boolean not null default true,
|
||||||
|
notes text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_calendar_access_email_not_blank check (length(trim(email)) > 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
create or replace function public.tax_normalize_access_email()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
new.email := lower(trim(new.email));
|
||||||
|
new.updated_at := now();
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_normalize_access_email_trigger on public.tax_calendar_access;
|
||||||
|
create trigger tax_normalize_access_email_trigger
|
||||||
|
before insert or update on public.tax_calendar_access
|
||||||
|
for each row execute function public.tax_normalize_access_email();
|
||||||
|
|
||||||
|
create or replace function public.has_tax_calendar_access()
|
||||||
|
returns boolean
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_calendar_access access
|
||||||
|
where access.email = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||||
|
and access.active = true
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.has_tax_calendar_access() from public, anon;
|
||||||
|
grant execute on function public.has_tax_calendar_access() to authenticated;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Catálogo de países
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_countries (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
code text not null unique,
|
||||||
|
name text not null unique,
|
||||||
|
is_regional boolean not null default false,
|
||||||
|
sort_order integer not null default 100,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
updated_by uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into public.tax_countries (code, name, is_regional, sort_order)
|
||||||
|
values
|
||||||
|
('REG', 'Regional', true, 0),
|
||||||
|
('CO', 'Colombia', false, 10),
|
||||||
|
('CR', 'Costa Rica', false, 20),
|
||||||
|
('SV', 'El Salvador', false, 30),
|
||||||
|
('GT', 'Guatemala', false, 40),
|
||||||
|
('HN', 'Honduras', false, 50),
|
||||||
|
('JM', 'Jamaica', false, 60),
|
||||||
|
('MX', 'México', false, 70),
|
||||||
|
('NI', 'Nicaragua', false, 80),
|
||||||
|
('PA', 'Panamá', false, 90),
|
||||||
|
('PR', 'Puerto Rico', false, 100),
|
||||||
|
('DO', 'República Dominicana', false, 110),
|
||||||
|
('TT', 'Trinidad y Tobago', false, 120)
|
||||||
|
on conflict (code) do update
|
||||||
|
set name = excluded.name,
|
||||||
|
is_regional = excluded.is_regional,
|
||||||
|
sort_order = excluded.sort_order,
|
||||||
|
active = true,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
drop trigger if exists tax_countries_updated_at_trigger on public.tax_countries;
|
||||||
|
create trigger tax_countries_updated_at_trigger
|
||||||
|
before update on public.tax_countries
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Obligaciones tributarias
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_obligations (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
due_date date not null,
|
||||||
|
description text not null,
|
||||||
|
category text not null check (category in ('nomina', 'admin')),
|
||||||
|
country_id uuid not null references public.tax_countries(id),
|
||||||
|
source text not null default 'app',
|
||||||
|
is_historical boolean not null default false,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_by uuid default auth.uid(),
|
||||||
|
updated_by uuid default auth.uid(),
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_obligations_description_not_blank check (length(trim(description)) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index if not exists tax_obligations_unique_active_event
|
||||||
|
on public.tax_obligations (due_date, country_id, lower(description))
|
||||||
|
where active = true;
|
||||||
|
|
||||||
|
drop trigger if exists tax_obligations_updated_at_trigger on public.tax_obligations;
|
||||||
|
create trigger tax_obligations_updated_at_trigger
|
||||||
|
before update on public.tax_obligations
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Contactos y canales
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_contacts (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
country_id uuid not null references public.tax_countries(id),
|
||||||
|
full_name text not null,
|
||||||
|
email text,
|
||||||
|
whatsapp_number text,
|
||||||
|
job_title text,
|
||||||
|
area text not null check (area in ('admin', 'nomina', 'regional')),
|
||||||
|
email_enabled boolean not null default true,
|
||||||
|
whatsapp_enabled boolean not null default true,
|
||||||
|
active boolean not null default true,
|
||||||
|
created_by uuid default auth.uid(),
|
||||||
|
updated_by uuid default auth.uid(),
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint tax_contacts_name_not_blank check (length(trim(full_name)) > 0),
|
||||||
|
constraint tax_contacts_email_when_enabled check (not email_enabled or length(trim(coalesce(email, ''))) > 3),
|
||||||
|
constraint tax_contacts_whatsapp_when_enabled check (not whatsapp_enabled or length(trim(coalesce(whatsapp_number, ''))) >= 8)
|
||||||
|
);
|
||||||
|
|
||||||
|
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, '')), '');
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_contacts_normalize_trigger on public.tax_contacts;
|
||||||
|
create trigger tax_contacts_normalize_trigger
|
||||||
|
before insert or update on public.tax_contacts
|
||||||
|
for each row execute function public.tax_normalize_contact();
|
||||||
|
|
||||||
|
create unique index if not exists tax_contacts_unique_active_email
|
||||||
|
on public.tax_contacts (country_id, lower(email))
|
||||||
|
where active = true and email is not null;
|
||||||
|
|
||||||
|
drop trigger if exists tax_contacts_updated_at_trigger on public.tax_contacts;
|
||||||
|
create trigger tax_contacts_updated_at_trigger
|
||||||
|
before update on public.tax_contacts
|
||||||
|
for each row execute function public.tax_set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Bitácora de envíos: evita recordatorios duplicados
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create table if not exists public.tax_notification_log (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
obligation_id uuid not null references public.tax_obligations(id),
|
||||||
|
contact_id uuid not null references public.tax_contacts(id),
|
||||||
|
alert_date date not null,
|
||||||
|
channel text not null check (channel in ('email', 'whatsapp')),
|
||||||
|
status text not null default 'sent' check (status in ('sent', 'failed')),
|
||||||
|
provider_response jsonb,
|
||||||
|
sent_at timestamptz not null default now(),
|
||||||
|
unique (obligation_id, contact_id, alert_date, channel)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Estado de envío y bloqueo de obligaciones notificadas
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
create or replace function public.tax_obligation_delivery_status()
|
||||||
|
returns table (
|
||||||
|
obligation_id uuid,
|
||||||
|
has_sent_notification boolean,
|
||||||
|
first_sent_at timestamptz,
|
||||||
|
sent_notification_count bigint
|
||||||
|
)
|
||||||
|
language sql
|
||||||
|
stable
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select
|
||||||
|
obligation.id as obligation_id,
|
||||||
|
count(log.id) filter (where log.status = 'sent') > 0 as has_sent_notification,
|
||||||
|
min(log.sent_at) filter (where log.status = 'sent') as first_sent_at,
|
||||||
|
count(log.id) filter (where log.status = 'sent') as sent_notification_count
|
||||||
|
from public.tax_obligations obligation
|
||||||
|
left join public.tax_notification_log log
|
||||||
|
on log.obligation_id = obligation.id
|
||||||
|
where obligation.active = true
|
||||||
|
and public.has_tax_calendar_access()
|
||||||
|
group by obligation.id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.tax_obligation_delivery_status()
|
||||||
|
from public, anon;
|
||||||
|
grant execute on function public.tax_obligation_delivery_status()
|
||||||
|
to authenticated;
|
||||||
|
|
||||||
|
create or replace function public.tax_prevent_sent_obligation_changes()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from public.tax_notification_log log
|
||||||
|
where log.obligation_id = old.id
|
||||||
|
and log.status = 'sent'
|
||||||
|
) and (
|
||||||
|
new.due_date is distinct from old.due_date
|
||||||
|
or new.description is distinct from old.description
|
||||||
|
or new.category is distinct from old.category
|
||||||
|
or new.country_id is distinct from old.country_id
|
||||||
|
or new.active is distinct from old.active
|
||||||
|
) then
|
||||||
|
raise exception using
|
||||||
|
errcode = 'P0001',
|
||||||
|
message = 'Esta obligación ya generó avisos y no puede modificarse ni eliminarse.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists tax_obligations_lock_after_sent_trigger
|
||||||
|
on public.tax_obligations;
|
||||||
|
|
||||||
|
create trigger tax_obligations_lock_after_sent_trigger
|
||||||
|
before update on public.tax_obligations
|
||||||
|
for each row execute function public.tax_prevent_sent_obligation_changes();
|
||||||
|
|
||||||
|
-- Miércoles estrictamente anterior a la fecha límite.
|
||||||
|
create or replace function public.tax_previous_wednesday(p_due_date date)
|
||||||
|
returns date
|
||||||
|
language sql
|
||||||
|
immutable
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
select p_due_date -
|
||||||
|
case
|
||||||
|
when mod((extract(dow from p_due_date)::integer - 3 + 7), 7) = 0 then 7
|
||||||
|
else mod((extract(dow from p_due_date)::integer - 3 + 7), 7)
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
comment on function public.tax_previous_wednesday(date) is
|
||||||
|
'Calcula el miércoles estrictamente anterior a una fecha límite.';
|
||||||
|
|
||||||
|
-- Devuelve una fila por obligación y destinatario pendiente de notificar.
|
||||||
|
create or replace 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,
|
||||||
|
contact_whatsapp text,
|
||||||
|
email_enabled boolean,
|
||||||
|
whatsapp_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.whatsapp_number as contact_whatsapp,
|
||||||
|
(
|
||||||
|
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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
and log.status = 'sent'
|
||||||
|
)
|
||||||
|
) as whatsapp_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.whatsapp_enabled
|
||||||
|
and contact.whatsapp_number 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 = 'whatsapp'
|
||||||
|
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;
|
||||||
|
|
||||||
|
create or replace function public.tax_record_notification(
|
||||||
|
p_obligation_id uuid,
|
||||||
|
p_contact_id uuid,
|
||||||
|
p_alert_date date,
|
||||||
|
p_channel text,
|
||||||
|
p_status text default 'sent',
|
||||||
|
p_provider_response jsonb default null
|
||||||
|
)
|
||||||
|
returns void
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.tax_notification_log (
|
||||||
|
obligation_id,
|
||||||
|
contact_id,
|
||||||
|
alert_date,
|
||||||
|
channel,
|
||||||
|
status,
|
||||||
|
provider_response,
|
||||||
|
sent_at
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
p_obligation_id,
|
||||||
|
p_contact_id,
|
||||||
|
p_alert_date,
|
||||||
|
p_channel,
|
||||||
|
p_status,
|
||||||
|
p_provider_response,
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
on conflict (obligation_id, contact_id, alert_date, channel)
|
||||||
|
do update set
|
||||||
|
status = excluded.status,
|
||||||
|
provider_response = excluded.provider_response,
|
||||||
|
sent_at = now();
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.tax_record_notification(uuid, uuid, date, text, text, jsonb)
|
||||||
|
from public, anon, authenticated;
|
||||||
|
grant execute on function public.tax_record_notification(uuid, uuid, date, text, text, jsonb)
|
||||||
|
to service_role;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Seguridad RLS para el frontend
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
alter table public.tax_calendar_access enable row level security;
|
||||||
|
alter table public.tax_countries enable row level security;
|
||||||
|
alter table public.tax_obligations enable row level security;
|
||||||
|
alter table public.tax_contacts enable row level security;
|
||||||
|
alter table public.tax_notification_log enable row level security;
|
||||||
|
|
||||||
|
revoke all on table public.tax_calendar_access from anon, authenticated;
|
||||||
|
revoke all on table public.tax_notification_log from anon, authenticated;
|
||||||
|
|
||||||
|
grant select on table public.tax_countries to authenticated;
|
||||||
|
grant select, insert, update on table public.tax_obligations to authenticated;
|
||||||
|
grant select, insert, update on table public.tax_contacts to authenticated;
|
||||||
|
|
||||||
|
drop policy if exists tax_countries_authorized_read on public.tax_countries;
|
||||||
|
create policy tax_countries_authorized_read
|
||||||
|
on public.tax_countries for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_read on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_read
|
||||||
|
on public.tax_obligations for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_insert on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_insert
|
||||||
|
on public.tax_obligations for insert
|
||||||
|
to authenticated
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_obligations_authorized_update on public.tax_obligations;
|
||||||
|
create policy tax_obligations_authorized_update
|
||||||
|
on public.tax_obligations for update
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access())
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_read on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_read
|
||||||
|
on public.tax_contacts for select
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_insert on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_insert
|
||||||
|
on public.tax_contacts for insert
|
||||||
|
to authenticated
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
drop policy if exists tax_contacts_authorized_update on public.tax_contacts;
|
||||||
|
create policy tax_contacts_authorized_update
|
||||||
|
on public.tax_contacts for update
|
||||||
|
to authenticated
|
||||||
|
using (public.has_tax_calendar_access())
|
||||||
|
with check (public.has_tax_calendar_access());
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Usuarios autorizados iniciales
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
insert into public.tax_calendar_access (email)
|
||||||
|
values
|
||||||
|
('ethen@gomezleemarketing.com'),
|
||||||
|
('iaracena@gomezleemarketing.com'),
|
||||||
|
('lmatos@gomezleemarketing.com'),
|
||||||
|
('asrodriguez@gomezleemarketing.com')
|
||||||
|
on conflict (email) do update
|
||||||
|
set active = true,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Contactos iniciales
|
||||||
|
-- Los números de WhatsApp quedan en formato internacional, sin el signo +.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
with contact_data(country_name, full_name, email, whatsapp_number, job_title, area) as (
|
||||||
|
values
|
||||||
|
('Guatemala', 'Narcisa Yessenia Barrera Godoy', 'administrativoguatemala2@gomezleemarketing.com', '50255581600', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('República Dominicana', 'Erika Benitez', 'administrativord@gomezleemarketing.com', '18099662004', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Honduras', 'Bessy Calix Pineda', 'administrativohonduras@gomezleemarketing.com', '50495890865', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Colombia', 'Natalia Casas Avella', 'administrativocolombia@gomezleemarketing.com', '573137702391', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Costa Rica', 'Fernanda Céspedes Vargas', 'admincr@gomezleemarketing.com', '50686236444', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Angie Chavez Silva', 'administrativonicaragua@gomezleemarketing.com', '50585263292', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Panamá', 'Jheysana Garaban Quintero', 'administrativopty2@gomezleemarketing.com', '50764548936', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('República Dominicana', 'Adrianita Gonzalez Figueroa', 'agonzalez@gomezleemarketing.com', '18094328347', 'Aux. Administrativo', 'admin'),
|
||||||
|
('El Salvador', 'Noemi Elizabeth Mena Medina', 'nmena02@gomezleemarketing.com', '50378102952', 'Aux. Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Estefani Orozco Davila', 'administrativo@gomezleemarketing.com', '50585306316', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Panamá', 'Carlos Paredes Garcia', 'cparedes@gomezleemarketing.com', '5073975475', 'Asistente Administrativo', 'admin'),
|
||||||
|
('Colombia', 'Lady Perdomo Suarez', 'administrativocolombia2@gomezleemarketing.com', '573222062462', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Guatemala', 'Carmen Estela Sierra Arias', 'administrativoguatemala@gomezleemarketing.com', '50258469211', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Guatemala', 'Shirley Velásquez González', 'administrativoguatemala3@gomezleemarketing.com', '50237727117', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Nicaragua', 'Seyling Lissamary Zambrana Reyes', 'auxiliaradmonnic@gomezleemarketing.com', '50584241616', 'Coordinador Administrativo', 'admin'),
|
||||||
|
('Regional', 'Ada Rodríguez', 'asrodriguez@gomezleemarketing.com', '50233335008', 'Coordinador Regional Administrativo', 'regional'),
|
||||||
|
('Regional', 'Zoeya Salomon Imbert', 'zsalomon@gomezleemarketing.com', '18293874206', 'Coordinador Regional Administrativo', 'regional'),
|
||||||
|
('Regional', 'Brayan Alexander Licona Oseguera', 'conciliaciones@gomezleemarketing.com', '50432077484', 'Coordinador Administrativo', 'regional'),
|
||||||
|
('Regional', 'Viviana Páramo Gonzalez', 'vparamo@gomezleemarketing.com', '573105474049', 'Subgerente Administrativo Regional', 'regional'),
|
||||||
|
('Regional', 'Mati Soto Valenzuela', 'msoto@gomezleemarketing.com', '18094037571', 'Nómina Regional', 'nomina'),
|
||||||
|
('Regional', 'Iveth Herrera', 'iherrera@gomezleemarketing.com', '50376373732', 'Nómina Regional', 'nomina')
|
||||||
|
)
|
||||||
|
insert into public.tax_contacts (
|
||||||
|
country_id,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
whatsapp_number,
|
||||||
|
job_title,
|
||||||
|
area,
|
||||||
|
email_enabled,
|
||||||
|
whatsapp_enabled
|
||||||
|
)
|
||||||
|
select
|
||||||
|
country.id,
|
||||||
|
data.full_name,
|
||||||
|
lower(data.email),
|
||||||
|
data.whatsapp_number,
|
||||||
|
data.job_title,
|
||||||
|
data.area,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
from contact_data data
|
||||||
|
join public.tax_countries country on country.name = data.country_name
|
||||||
|
on conflict do nothing;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Migración de las 294 obligaciones históricas del calendario 2026
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
with obligation_data(due_date, description, category, country_name) as (
|
||||||
|
values
|
||||||
|
('2026-01-02'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-01-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-01-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-01-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-01-12'::date, 'Puerto Rico: IVU Impuestos sobre ventas y uso)', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-01-14'::date, 'Panamá: IVA / Municipio', 'admin', 'Panamá'),
|
||||||
|
('2026-01-14'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-01-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-01-14'::date, 'HN: INFOP', 'nomina', 'Honduras'),
|
||||||
|
('2026-01-14'::date, 'NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-15'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-01-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-01-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-01-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-01-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-01-21'::date, 'COL: Retencion en la fuente', 'admin', 'Colombia'),
|
||||||
|
('2026-01-21'::date, 'COL: IVA', 'admin', 'Colombia'),
|
||||||
|
('2026-01-21'::date, 'HN: IHSS y RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-01-22'::date, 'Nicaragua Paga Matricula 2024', 'admin', 'Nicaragua'),
|
||||||
|
('2026-01-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-01-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-01-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-01-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-01-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales; ISO (Anual)', 'admin', 'Guatemala'),
|
||||||
|
('2026-01-30'::date, 'Puerto Rico: Formularios Anuales (W-2, 1099, Reconciliaciones patronales)', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-02-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-02-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-02-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-02-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-02-12'::date, 'Panamá: IVA / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-02-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-02-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-02-13'::date, '- Guatemala: Impuestos de Retenciones /', 'admin', 'Guatemala'),
|
||||||
|
('2026-02-13'::date, '- NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-02-19'::date, 'COL: Retencion fuente (Ene)', 'admin', 'Colombia'),
|
||||||
|
('2026-02-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-02-20'::date, 'COL: ICA BIM 6', 'admin', 'Colombia'),
|
||||||
|
('2026-02-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-02-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-02-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-02-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-02-26'::date, 'COL: ICA Anual 2024', 'admin', 'Colombia'),
|
||||||
|
('2026-02-26'::date, 'Nicaragua-Pago de IR Anual', 'admin', 'Nicaragua'),
|
||||||
|
('2026-02-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-02-27'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-03-02'::date, 'Jamaica: Anual Return and beneficial ownership', 'admin', 'Jamaica'),
|
||||||
|
('2026-03-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-03-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-03-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-03-07'::date, 'Jamaica: Anual Income Tax (1st Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-03-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-03-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-03-12'::date, 'Panamá: IVA / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-03-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-03-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-03-13'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-03-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-03-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-03-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-03-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-03-19'::date, 'COL: Retencion fuente (Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-03-19'::date, 'COL:IVA', 'admin', 'Colombia'),
|
||||||
|
('2026-03-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-03-20'::date, 'COL: Rete Ica Bimestre 1 (Ene-Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-03-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-03-24'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-03-26'::date, 'Costa Rica: RENTA 2023, INS IITrimestre', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-03-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-03-29'::date, 'Panamá: Declaracion de renta', 'admin', 'Panamá'),
|
||||||
|
('2026-04-03'::date, 'COL: ICA 1(Ene-Feb)', 'admin', 'Colombia'),
|
||||||
|
('2026-04-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-04-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-04-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Planilla de Contribucion sobre Ingresos', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Quarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-04-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR / NICARAGUA: INSS e INATEC', 'nomina', 'Honduras'),
|
||||||
|
('2026-04-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-04-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-04-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-04-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-04-16'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-04-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-04-17'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-18'::date, 'SEMANA SANTA', 'admin', 'Regional'),
|
||||||
|
('2026-04-20'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-04-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-04-21'::date, 'COL: Retencion fuente (Marzo) HN: RAP', 'nomina', 'Colombia'),
|
||||||
|
('2026-04-23'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-04-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-04-29'::date, 'Trinidad: Anual Income Tax Return', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-05-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-05-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-05-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-05-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-05-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-05-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC / NICARAGUA: IMPUESTO IMI ALCALDIA', 'nomina', 'Panamá'),
|
||||||
|
('2026-05-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-05-16'::date, 'MX: Liquidaciones al IMSS e INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-05-16'::date, 'MX: Impuesto Sobre nóminas CDMX', 'nomina', 'México'),
|
||||||
|
('2026-05-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-05-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-05-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-05-20'::date, 'COL: Renta Anual (01 cuota)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-20'::date, 'COL: Iva (Ene-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-20'::date, 'COL: Retencion fuente (Abril)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-05-22'::date, 'COL: ReteIca Bimestre 2 (Mar-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-05-22'::date, 'MX: Retenciones de ISR e IVA', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-05-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-05-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-06-01'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-06-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-06-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-06-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-06-07'::date, 'Jamaica: Anual Income Tax (2nd Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-06-07'::date, 'Puerto Rico: Qarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-06-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-06-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-06-11'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-06-11'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-06-12'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-06-12'::date, 'COL: ICA 2(Mar-Abr)', 'admin', 'Colombia'),
|
||||||
|
('2026-06-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-06-12'::date, 'ESV: IVA, Retencion en la fuente, AFP Crecer, AFP Confia', 'nomina', 'El Salvador'),
|
||||||
|
('2026-06-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-06-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-06-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-06-19'::date, 'COL: Retencion fuente (May)', 'admin', 'Colombia'),
|
||||||
|
('2026-06-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-06-22'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-06-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-06-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-06-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-06-29'::date, 'Panamá: Seguridad social / 1ra pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-06-29'::date, 'CostaRica: 1er anticipo de Renta, INS IIIer trimestre', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-06-29'::date, 'TT: Pago Estimada de renta Q1 2025', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-06-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-07-03'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-07-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-07-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-07-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-07-07'::date, 'Puerto Rico: Patente Municipal', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-07-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-07-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-07-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-07-14'::date, 'Panamá: IVA / Municipio / Tasa Unica / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-07-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-07-14'::date, 'ESV: IVA, Retencion en la fuente, AFP Crecer, AFP Confia', 'nomina', 'El Salvador'),
|
||||||
|
('2026-07-16'::date, 'NICARAGUA: INSS e INATECI', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-07-16'::date, 'MX: Impuesto Sobre NóminasIMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-07-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-07-17'::date, 'COL: ReteIca Bimestre 3(May-Jun)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'COL: Retencion fuente (Jun)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'COL: Renta Anual (02 cuota)', 'admin', 'Colombia'),
|
||||||
|
('2026-07-17'::date, 'Trinidad: Value Added Tax', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-07-20'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-07-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-07-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-07-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-07-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-07-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-07-29'::date, 'Panamá: Seguridad social / Planilla 03', 'admin', 'Panamá'),
|
||||||
|
('2026-07-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-08-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-08-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-08-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-08-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-08-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-08-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-08-13'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-08-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-08-13'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-08-14'::date, 'COL: ICA 3(May-Jun) / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Colombia'),
|
||||||
|
('2026-08-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-08-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-08-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-08-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-08-21'::date, 'COL: Retencion fuente (Jul) HN: RAP', 'nomina', 'Colombia'),
|
||||||
|
('2026-08-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-08-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-08-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-08-28'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-08-31'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-09-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-09-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-09-07'::date, 'Jamaica: Anual Income Tax (3rd Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-09-07'::date, 'Puerto Rico: Quarterly Estimated Tax', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-09-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-09-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-09-10'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-09-11'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-09-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-09-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-09-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-09-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-09-17'::date, 'COL: Iva (May-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-17'::date, 'COL: Retencion Fuente (Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-17'::date, 'Trinidad: Value added Tax', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-09-18'::date, 'COL: ReteIca Bimestre 4(Jul-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-09-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-09-21'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-09-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-09-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-09-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-09-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-09-29'::date, 'Panamá: Seguridad social / 2da pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-09-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-09-29'::date, 'Costa Rica: II anticipo de Renta IVtrimestre del INS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-09-29'::date, 'Trinidad: Quarterly Installment Payments', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-10-02'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-10-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-10-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-10-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-10-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-10-09'::date, 'COL: ICA 4(Jul-Ago)', 'admin', 'Colombia'),
|
||||||
|
('2026-10-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-10-13'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-10-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-10-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-10-15'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-10-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-10-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-10-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-10-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-10-20'::date, 'COL: Retencion fuente (Sep)', 'admin', 'Colombia'),
|
||||||
|
('2026-10-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-10-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-10-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-10-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-10-29'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-10-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-11-05'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-11-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-11-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-11-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-11-09'::date, 'HN: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-11-12'::date, 'Panamá: IVA / Municipio / NICARAGUA: INSS e INATEC', 'nomina', 'Panamá'),
|
||||||
|
('2026-11-12'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-11-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-11-13'::date, 'Guatemala: Impuestos de Retenciones / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Guatemala'),
|
||||||
|
('2026-11-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-11-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-11-17'::date, 'Trinidad: Value Aded tax (Vat)', 'admin', 'Trinidad y Tobago'),
|
||||||
|
('2026-11-19'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-11-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-20'::date, 'COL: ReteIca Bimestre 5(Sep-Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-23'::date, 'COL: Retencion Fuente (Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-23'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-11-23'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-11-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-11-27'::date, 'Panamá: Seguridad social', 'admin', 'Panamá'),
|
||||||
|
('2026-11-30'::date, 'COL: Retencion Fuente (Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-11-30'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-11-30'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'MX: Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-11-30'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-04'::date, 'Nicaragua-Pago de DGI', 'admin', 'Nicaragua'),
|
||||||
|
('2026-12-06'::date, 'Jamaica: GCT Monthly Statutory Deductions', 'nomina', 'Jamaica'),
|
||||||
|
('2026-12-07'::date, 'Trinidad: PAYE, NIS, and Health Surcharge', 'nomina', 'Trinidad y Tobago'),
|
||||||
|
('2026-12-07'::date, 'Jamaica: Anual Income Tax (4th Installment)', 'nomina', 'Jamaica'),
|
||||||
|
('2026-12-07'::date, 'Puerto Rico: Nomina y Retenciones Patronales', 'nomina', 'Puerto Rico'),
|
||||||
|
('2026-12-09'::date, 'Honduras: ISV, Ret 12.5%, Ret 1%, ISR', 'admin', 'Honduras'),
|
||||||
|
('2026-12-10'::date, 'NICARAGUA: INSS e INATEC', 'nomina', 'Nicaragua'),
|
||||||
|
('2026-12-11'::date, 'Guatemala: Impuestos de Retenciones', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-11'::date, 'COL: ICA 5(Sep-Oct)', 'admin', 'Colombia'),
|
||||||
|
('2026-12-12'::date, 'Puerto Rico: IVU Impuesto sobre ventas y uso', 'admin', 'Puerto Rico'),
|
||||||
|
('2026-12-14'::date, 'Panamá: IVA / Municipio / NICARAGUA: IMPUESTO IMI ALCALDIA', 'admin', 'Panamá'),
|
||||||
|
('2026-12-14'::date, 'Costa Rica:IVA, Rete fuente y CCSS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-12-16'::date, 'MX: IMSS, SAR, INFONAVIT', 'nomina', 'México'),
|
||||||
|
('2026-12-16'::date, 'MX: Impuesto Sobre Nóminas', 'nomina', 'México'),
|
||||||
|
('2026-12-18'::date, 'COL: Retencion Fuente (Nov)', 'admin', 'Colombia'),
|
||||||
|
('2026-12-20'::date, 'HN: IHSS', 'nomina', 'Honduras'),
|
||||||
|
('2026-12-21'::date, 'Guatemala: IGSS', 'nomina', 'Guatemala'),
|
||||||
|
('2026-12-21'::date, 'HN: RAP', 'nomina', 'Honduras'),
|
||||||
|
('2026-12-23'::date, 'MX: Retenciones de ISR por sueldos', 'admin', 'México'),
|
||||||
|
('2026-12-23'::date, 'MX; Retenciones de ISR por honorarios y arrendamiento', 'admin', 'México'),
|
||||||
|
('2026-12-23'::date, 'MX: IVA a cargo (a favor)', 'admin', 'México'),
|
||||||
|
('2026-12-29'::date, 'Panamá: Seguridad social / 3ra pardida de ISR', 'admin', 'Panamá'),
|
||||||
|
('2026-12-29'::date, 'Guatemala: IVA General; IVA Facturas Especiales;', 'admin', 'Guatemala'),
|
||||||
|
('2026-12-29'::date, 'Costa Rica: III anticipo de Renta, Ier trimestre 2026 INS', 'nomina', 'Costa Rica'),
|
||||||
|
('2026-12-30'::date, 'Trinidad: Quarterly Installment Payment', 'nomina', 'Trinidad y Tobago')
|
||||||
|
)
|
||||||
|
insert into public.tax_obligations (
|
||||||
|
due_date,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
country_id,
|
||||||
|
source,
|
||||||
|
is_historical
|
||||||
|
)
|
||||||
|
select
|
||||||
|
data.due_date,
|
||||||
|
data.description,
|
||||||
|
data.category,
|
||||||
|
country.id,
|
||||||
|
'excel_2026',
|
||||||
|
true
|
||||||
|
from obligation_data data
|
||||||
|
join public.tax_countries country on country.name = data.country_name
|
||||||
|
on conflict do nothing;
|
||||||
|
|
||||||
|
commit;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Operaciones administrativas frecuentes
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Agregar/reactivar acceso:
|
||||||
|
-- insert into public.tax_calendar_access (email, full_name)
|
||||||
|
-- values ('nuevo@gomezleemarketing.com', 'Nombre Apellido')
|
||||||
|
-- on conflict (email) do update set active = true, full_name = excluded.full_name;
|
||||||
|
|
||||||
|
-- Quitar acceso sin borrar historial:
|
||||||
|
-- update public.tax_calendar_access
|
||||||
|
-- set active = false
|
||||||
|
-- where email = 'persona@gomezleemarketing.com';
|
||||||
|
|
||||||
|
-- Prueba de recordatorios de una fecha:
|
||||||
|
-- select * from public.tax_due_reminders('2026-07-24'::date);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts", "eslint.config.js"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "/calendario-impuestos/",
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user