Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AdriP-maker/JAAR_Antigravity/llms.txt

Use this file to discover all available pages before exploring further.

Why gamification?

Rural water boards depend on two pillars: consistent monthly fees and community labor (jornales). SIMAP Digital’s points engine creates positive reinforcement for both. Instead of relying solely on fines and service cuts, the system rewards residents who pay on time and show up to community workdays. This transforms collection dynamics from purely punitive to a blend of incentives and accountability that rural Juntas have found culturally effective. The points system is defined in RF-19 and RF-20 of the SIMAP requirements spec and is implemented in src/services/puntosService.js.

How points are earned

Every qualifying action writes a ganancia record to the puntos_historial table in the local IndexedDB store. The table below lists all earning events:
EventPoints awarded
Basic monthly payment2 pts
Punctual payment (paid before the 15th)5 pts
Multi-month or advance payment10 pts per extra month covered
Personal jornal attendance8 pts
Substitute attendance3 pts
Jornal confirmation submitted in advance2 pts
Full quarter with zero fines15 pts
Full calendar year paid without gaps30 pts
Points are awarded automatically by the service layer at the moment the payment or jornal attendance is recorded — the cobrador does not need to manually grant them.

Redemption rules

Points convert to Balboa discounts at a fixed rate:
  • 1 point = B/.0.01 (one centavo)
  • Minimum redemption: 10 points (= B/.0.10)
  • Maximum discount per month: 150 points (= B/.1.50)
Important — commission integrity (RF-20): The cobrador’s commission is always calculated on the full tariff, not on the discounted amount after points are applied. A resident who redeems B/.1.50 in points off a B/.3.00 bill still generates the full B/.3.00 commission basis. This protects cobrador earnings while rewarding residents.

Utility functions

Two pure functions in puntosService.js handle the conversion math:
// src/services/puntosService.js

/**
 * Convert a points balance to its Balboa value.
 * 100 points = B/.1.00  →  1 point = B/.0.01
 */
export function calcularDescuentoBalboas(puntos) {
  return puntos / 100;
}

/**
 * Convert a Balboa amount back into points (for display or validation).
 */
export function balboasAPuntos(balboas) {
  return Math.floor(balboas * 100);
}
These functions are imported directly by CobroModal.jsx to compute the live discount preview as the cobrador toggles the “Usar puntos” checkbox.

How to redeem: the CobroModal flow

Redemption happens inside the payment modal (src/components/cobros/CobroModal.jsx). The cobrador opens the modal for a resident who has a positive points balance and follows these steps:
1

Open the Cobro Modal

From the /cobros screen, tap the resident’s row. The modal loads their payment summary and fetches their current points balance via obtenerSaldoPuntos(userId).
2

Review the points banner

If the resident has any points, a gold banner appears beneath the payment-type tabs:
🌟 Tienes 120 Puntos SIMAP — Equivale a B/.1.20 de descuento.
The discount amount is calculated live using calcularDescuentoBalboas(puntos).
3

Toggle 'Usar puntos'

The cobrador checks the Usar puntos checkbox. The final amount displayed in the cobro total updates immediately — the discount is subtracted from montoFinal before it is shown:
if (usarPuntos && puntos > 0) {
  const descuento = calcularDescuentoBalboas(puntos);
  monto = Math.max(0, redondear(monto - descuento));
}
4

Confirm the payment

The cobrador taps Confirmar Cobro. The system calls registrarPago() with the discounted amount, then immediately calls canjearPuntos() to write the redemption record:
await registrarPago(user.id, { tipo, monto: montoFinal, nota });

if (usarPuntos && puntos > 0) {
  await canjearPuntos(user.id, puntos, 'Descuento aplicado en cobro');
}

Core service functions

// src/services/puntosService.js

/**
 * Award points to a resident for a positive action.
 * Writes a 'ganancia' record to puntos_historial.
 */
export async function otorgarPuntos(usuarioId, cantidad, motivo) {
  if (cantidad <= 0) return;

  await db.puntos_historial.add({
    usuarioId: String(usuarioId),
    cantidad: cantidad,
    tipo_transaccion: 'ganancia',
    motivo: motivo,        // e.g. 'Pago puntual antes del día 15'
    fecha: new Date().toISOString()
  });
}

/**
 * Redeem points as a bill discount.
 * Writes a 'canje' record and throws if the balance is insufficient.
 */
export async function canjearPuntos(usuarioId, cantidad, motivo = 'Descuento en cobro') {
  const saldoActual = await obtenerSaldoPuntos(usuarioId);
  if (cantidad > saldoActual) {
    throw new Error('No hay suficientes puntos para canjear');
  }

  await db.puntos_historial.add({
    usuarioId: String(usuarioId),
    cantidad: cantidad,
    tipo_transaccion: 'canje',
    motivo: motivo,
    fecha: new Date().toISOString()
  });
}

The puntos_historial table

All point activity lives in a single append-only table in the local IndexedDB (Dexie.js) store. There are exactly two transaction types:
tipo_transaccionMeaning
gananciaPoints earned — added to the running balance
canjePoints redeemed — subtracted from the running balance
No records are ever deleted or modified. The current balance is always a fresh derivation from the full history.
The live points balance (obtenerSaldoPuntos) is computed by iterating all puntos_historial rows for a resident and summing ganancia entries while subtracting canje entries. There is no cached “balance” column — the ledger is the source of truth. This design is intentional: it keeps the history immutable and tamper-evident, consistent with SIMAP’s broader audit philosophy.
export async function obtenerSaldoPuntos(usuarioId) {
  const historial = await db.puntos_historial
    .where('usuarioId').equals(String(usuarioId))
    .toArray();

  let saldo = 0;
  for (const h of historial) {
    if (h.tipo_transaccion === 'ganancia') {
      saldo += h.cantidad;
    } else if (h.tipo_transaccion === 'canje') {
      saldo -= h.cantidad;
    }
  }
  return saldo;
}

Admin configuration

The administrator can tune the incentive system from ConfigPage (/puntos-admin, accessible to the admin role). The relevant fields in the “Reglas de Fidelidad (Puntos)” card are:
Config fieldDefaultDescription
puntosPorPagoPuntual10Points granted for an on-time payment
puntosRequeridosDescuento100Points threshold required to unlock a redemption
descuentoGeneradoB/.3.00Balboa value of the discount applied when a resident redeems
Changes are persisted via saveConfig() to the local store and synced to Supabase when the device comes online. The ConfigPage validates that commission splits (splitDevs + splitCobrador) always equal exactly 1.0 (100%) before saving.

Build docs developers (and LLMs) love