Versión inicial de Seguimiento de Impuestos GLM

This commit is contained in:
2026-07-25 10:01:55 -04:00
commit b0bf624ad5
33 changed files with 9851 additions and 0 deletions
+9
View File
@@ -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
View File
@@ -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?
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
package-lock.json
+6
View File
@@ -0,0 +1,6 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": false,
"trailingComma": "all"
}
+13
View File
@@ -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`.
+88
View File
@@ -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.
+27
View File
@@ -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
View File
@@ -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>
+37
View File
@@ -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
+3812
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -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

+5
View File
@@ -0,0 +1,5 @@
import AuthGate from "./auth/AuthGate";
export default function App() {
return <AuthGate />;
}
+1878
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+309
View File
@@ -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.";
}
+266
View File
@@ -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);
}
+366
View File
@@ -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.
}
}
+90
View File
@@ -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();
}
+12
View File
@@ -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
View File
@@ -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;
}
+12
View File
@@ -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;
}
+50
View File
@@ -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.
+932
View File
@@ -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;
+932
View File
@@ -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);
+21
View File
@@ -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"]
}
+8
View File
@@ -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()],
});