Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ config.load()
4. **Compute diff** — see §8.
5. **Safety check** — see §9.
6. **Apply diff** — `unifi.apply(diff)` iterates the diff sets serially with a 50-100ms inter-call delay. Each action retried per design guide §8 (exponential backoff, honor `Retry-After` on 429). Failures partway through are tolerable: idempotency means the next cycle resumes correctly.

**Per-user isolation:** a single contact's `UnifiClientError` is recorded and the cycle continues with the remaining contacts; `apply()` then raises one summary error so the orchestrator alerts. One bad record never blocks the rest. (The batch card pre-import is a shared prerequisite and still fails fast.)

**Email is best-effort:** UniFi requires globally-unique emails across users *and* admins. When a member's CiviCRM email is already registered to another account — commonly a staff member who is also a UniFi admin — the write is retried without the `user_email` field (so name/card/policy still apply) and a warning is logged. The member keeps door access; only the conflicting email isn't synced. This is *not* counted as a per-user failure, so it doesn't alert.
7. **Log + persist state** — write audit entries; write last-success timestamp atomically (write to temp, fsync, rename).

---
Expand Down
11 changes: 10 additions & 1 deletion docs/architecture/conventions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ exceptions on data issues. They return sentinel values — for example,
orchestrator decide how to handle them.

**Clients** (``civicrm.client``, ``unifi.client``) raise after exhausting
retries. Client exceptions propagate through the orchestrator to the scheduler.
retries. Client exceptions propagate through the orchestrator to the scheduler,
with two refinements inside ``unifi.apply()``:

- **Per-user isolation.** A single contact's ``UnifiClientError`` is logged,
recorded, and skipped so the remaining contacts still apply; ``apply()`` then
raises one summary error at the end so the failure is still surfaced.
- **Best-effort email.** UniFi requires globally-unique emails across users and
admins, so an email already registered to another account is dropped from the
write (the rest of the record still applies) and warned — not treated as a
failure.

**The scheduler** catches per-cycle exceptions, logs them, writes a crash
audit record, and continues to the next cycle.
Expand Down
6 changes: 5 additions & 1 deletion docs/architecture/reconciliation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ Key design properties:
- **Clients are per-cycle.** They're cheap to construct and this gives clean
isolation between cycles, avoiding stale HTTP sessions.
- **Exceptions propagate.** The orchestrator does not catch — the scheduler's
per-cycle ``try/except`` handles crashes.
per-cycle ``try/except`` handles crashes. Within ``apply()`` itself, a single
contact's failure is isolated (logged and skipped) so the rest of the cycle
still applies; a summary error is then raised so the failure still surfaces.
Email writes are best-effort: an address already registered to another UniFi
account is dropped and warned rather than failing the contact.
- **One function, many callers.** The same ``reconcile()`` is called by the
daemon loop, the ``--once`` CLI mode, and (in the future) the webhook handler.
13 changes: 12 additions & 1 deletion docs/superpowers/specs/2026-06-17-sync-member-email-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ provisioned and kept in sync on the UniFi Access user record.

---

## 1. Motivation
> **Correction (2026-06-29) — email writes are best-effort.**
>
> This spec describes writing `user_email` on create/reactivate/update (§3.5)
> but did not account for UniFi's constraint that **emails are globally unique
> across users *and* admins**. In production this surfaced as a
> `CODE_ADMIN_EMAIL_EXIST` crash for members who are also UniFi admins. The
> implemented behavior: a write rejected with an `*_EMAIL_EXIST` code is retried
> **without** the `user_email` field (so name/card/policy still apply) and a
> warning is logged; the email simply isn't synced for that member, and it is
> not treated as a cycle failure. Per-user failures during `apply()` are also
> isolated now (one bad contact no longer halts the rest). See
> `docs/architecture.md` §7 and `architecture/conventions.rst` "Error Handling".

UniFi Access uses a user's email **functionally**: it delivers mobile
credential invites and PIN codes to that address. Today the reconciler syncs
Expand Down
Loading