> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# loyalty.badge_award

> Qué insignia ganó quién y cuándo. Append-only: una insignia otorgada no se quita.

export const ParamField = ({default: defaultValue, deprecated = false, children, name, required = false, rules, type, unique = false, base = false, visibility = 'public'}) => <div className="pt-2.5 pb-5 my-2.5 border-gray-50 dark:border-gray-800/50 border-b">
    <div className="flex font-mono text-sm group/param-head param-head">
      <div className="flex-1 flex content-start py-0.5 mr-5">
        <div className="flex items-center flex-wrap gap-2">
          <div className="absolute h-0 w-0">
            <a href={`#param-${String(name).replace(/_/g, '-')}`} className="-ml-10 flex items-center opacity-0 border-0 group-hover/param-head:opacity-100 py-2 [.expandable-content_&]:-ml-[2.1rem]" aria-label="Ir a este campo">
              <div className="w-6 h-6 text-gray-400 rounded-md flex items-center justify-center zinc-box bg-white ring-1 ring-gray-400/30 dark:ring-gray-700/25 hover:ring-gray-400/60 dark:hover:ring-white/20">
                <svg xmlns="http://www.w3.org/2000/svg" fill="gray" height="12px" viewBox="0 0 576 512">
                  <path d="M0 256C0 167.6 71.6 96 160 96h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C98.1 144 48 194.1 48 256s50.1 112 112 112h72c13.3 0 24 10.7 24 24s-10.7 24-24 24H160C71.6 416 0 344.4 0 256zm576 0c0 88.4-71.6 160-160 160H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c61.9 0 112-50.1 112-112s-50.1-112-112-112H344c-13.3 0-24-10.7-24-24s10.7-24 24-24h72c88.4 0 160 71.6 160 160zM184 232H392c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24s10.7-24 24-24z"></path>
                </svg>
              </div>
            </a>
          </div>
          <div id={`param-${String(name).replace(/_/g, '-')}`} className="font-semibold text-primary dark:text-primary-light cursor-pointer">{name}</div>
          <div className="flex items-center space-x-2 text-xs font-medium">
            {type && <span className="px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-gray-600 dark:text-gray-200 font-medium">
                {type}
              </span>}
            {defaultValue && <span className="px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-gray-600 dark:text-gray-200 font-medium">
                <span className="text-gray-400 dark:text-gray-500">default:</span>"{defaultValue}"
              </span>}
            {base && <span className="px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-gray-500 dark:text-gray-400 font-medium">
                base
              </span>}
            {visibility && <span className="px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-blue-600 dark:text-blue-300 font-medium">
                {visibility === 'private' ? 'privado' : 'público'}
              </span>}
            {unique && <span className="px-2 py-0.5 rounded-md bg-gray-100/50 dark:bg-white/5 text-blue-600 dark:text-blue-300 font-medium">
                único
              </span>}
            {required && <span className="px-2 py-0.5 rounded-md bg-red-100/50 dark:bg-red-400/10 text-red-600 dark:text-red-300 font-medium">
                obligatorio
              </span>}
            {deprecated && <span className="px-2 py-0.5 rounded-md bg-amber-100/50 dark:bg-amber-400/10 text-amber-600 dark:text-amber-300 font-medium">
                deprecado
              </span>}
          </div>
        </div>
      </div>
    </div>
    {children && <div className="mt-4 prose-sm prose-gray dark:prose-invert [&_.prose>p:first-child]:mt-0 [&_.prose>p:last-child]:mb-0">
        {children}
      </div>}
    {rules && <div className="mt-2 prose-sm prose-gray dark:prose-invert [&_.prose>p:first-child]:mt-0 [&_.prose>p:last-child]:mb-0">
        <b>Reglas</b>: <span>{rules}</span>
      </div>}
  </div>;

**Forma B — append-only.** Una insignia otorgada **no se quita**, ni siquiera si el member deja de
cumplir la condición después. Es un reconocimiento de algo que pasó, no un estado.

Quitarla destruiría exactamente el efecto psicológico —progreso dotado— por el que la gamificación
funciona.

## Clave primaria

<ParamField default={'uuidv7'} name={'id'} required={true} type={'uuid'} base={true}>
  Identificador de la entrega.
</ParamField>

## Atributos específicos

<ParamField name={'awarded_at'} required={true} default={'now()'} rules={'Con zona horaria'} type={'timestamptz'}>
  Cuándo se ganó. Columna de tiempo del dominio.
</ParamField>

## Claves foráneas

<ParamField name={'badge_id'} required={true} type={'uuid'}>
  [Ref: badge](/schemas/loyalty/badge)
</ParamField>

<ParamField name={'contact_id'} required={true} type={'uuid'}>
  [Ref: core.contact](/schemas/core/contact)
</ParamField>

<ParamField name={'program_id'} required={true} type={'uuid'}>
  El programa dueño. Presente en **toda** tabla de `loyalty` desde la primera migración (ADR-021).
  [Ref: program](/schemas/loyalty/program)
</ParamField>

<ParamField name={'tenant_id'} required={true} type={'uuid'} base={true}>
  El tenant dueño. Toda restricción de unicidad de esta tabla lo incluye (`standards/data.md` §2b).
</ParamField>

## Atributos base

<ParamField name={'cell_id'} required={true} type={'uuid'} base={true}>
  Costura de particionamiento regional.
</ParamField>

<ParamField default={'now()'} name={'created_at'} required={true} rules={'Con zona horaria'} type={'timestamptz'} base={true}>
  Forma B: no hay `updated_at` ni `deleted_at`. Una fila append-only no se actualiza ni se borra.
</ParamField>

## Restricciones e índices

| Nombre                    | Definición                                             | Por qué existe                    |
| ------------------------- | ------------------------------------------------------ | --------------------------------- |
| `uq_badge_award`          | `UNIQUE (tenant_id, badge_id, contact_id)`             | Una insignia se gana una vez      |
| `idx_badge_award_contact` | `(tenant_id, program_id, contact_id, awarded_at DESC)` | El perfil del member en su portal |

## RLS y particionamiento

**RLS activa** por `tenant_id`. No particionada.

## Registro de cambios

| Fecha      | Cambio                                                       | Por qué                                                                                         | Decidido en | Autor                  |
| ---------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ----------- | ---------------------- |
| 2026-08-17 | Tabla creada como append-only                                | Quitar una insignia destruiría el efecto de progreso dotado por el que la gamificación funciona | FS-LOY-0012 | daniel + claude-opus-5 |
| 2026-08-18 | Renombrada de `loyalty.badge_awards` a `loyalty.badge_award` | Nombre en singular                                                                              | ADR-024     | daniel + claude-opus-5 |
