# ADR 0008. At-Least-Once Delivery + Idempotent Processing (Not Exactly-Once)

## Status
Accepted.

Uniqueness keys follow spec §38.1, not the earlier `source_party_id` pairing:

```text
batch uniqueness:  (integration_id, batch_id)
event uniqueness:  (integration_id, event_id)
ledger uniqueness: (campaign_rule_version_id, activity_event_id, ledger_action)
```

Identical retry returns the original receipt. Reused ID with a different
checksum returns `409 idempotency_conflict`.

## Context
Exactly-once delivery is expensive to guarantee end-to-end and usually
unnecessary if processing is idempotent. Given async, multi-hop
processing (ingestion -> log -> reward worker -> ledger), some
duplicate delivery is expected in failure/retry scenarios.

## Decision
Accept at-least-once delivery. Enforce idempotency explicitly at two
levels:
- Ingestion: `UNIQUE (integration_id, event_id)` on activity events, plus
  `UNIQUE (integration_id, batch_id)` on batches (spec §38.1).
- Reward/ledger side: `UNIQUE (campaign_rule_version_id, activity_event_id,
  ledger_action)` so the same activity cannot apply the same ledger action
  twice.

A duplicate delivery is treated as a successful no-op (identical retry
returns the original receipt; spec §38.1). It is not an error requiring
partner intervention.

## Consequences
- Simpler infrastructure than exactly-once semantics would require.
- Partners can safely retry on ambiguous failures (timeout, connection
  drop) without risking duplicate rewards.
- Raw activity is corrected via adjustment/reversal/compensating events,
  never by mutating or deleting history. See [docs/loyalty.md](../loyalty.md).
