The account's primary number is a way back in #144

Closed
opened 2026-09-09 10:48:00 +00:00 by tiagoagueda · 1 comment
Owner

Observation

multiple contacts plugin should in intrinsic link to the account, the primary contact can be
used as recovery route when sms notifications is written on the code

What exists

The intrinsic link is already there, and it is worth being precise about its shape. A
PhoneNumber has an owner — the account — and a holder, which is a generic relation to
either that person's Profile or a Contact at a company. So every row belongs to an
account, and the account's own numbers are the ones whose holder is their profile. Exactly
one of those is is_primary, enforced by a partial unique index rather than by a form.

Numbers are unique across the instance, which was a deliberate choice made against the
alternative, and they are unverified, which was a consequence of having no channel to
verify them over.

The recovery machinery is already written to accept a new route. recovery_routes()
returns a list, refuse_switching_off() reads it, and the docstring says a route that lands
is added there and "the lock below opens by itself". Nothing needs redesigning to admit
one — which is why the interesting work is entirely in the two prerequisites.

What this asks for

The account's primary number as a way back in, joining email and the passkey.

Worth being careful about

The choice made in #90 turns out to have been the right one for this. Instance-wide
uniqueness was argued about and taken with its disclosure named; a number that identifies an
account for recovery has to be unique, so the rule that looked like tidiness is now
load-bearing. Worth recording that it was not chosen for this reason and holds anyway.

Primary is a property of a holder, not of an account. Each contact has a primary number
too, and a recovery lookup that reads is_primary without filtering the holder would happily
find a recruiter's mobile. Anything here filters on the profile first.

The number that gets somebody back in should probably not be the one on their CV. The
primary is what a document prints — a recruiter dials it. Making the same row the recovery
route couples "the number I publish" to "the number that proves I am me", and somebody who
changes the number on their CV changes their recovery route without being told. Either the
recovery number is chosen separately, or changing the primary says out loud what else it
changes.

Switching the plugin off must not remove a way back in. #90's rule is that off shows the
primary and keeps the rest; if the primary is a recovery route, off has to keep it working
as one while hiding the others. And the account-recovery interlock (#104, #103) must count it
regardless of any per-person plugin state, or an administrator switching the feature off for
somebody quietly removes their route — which is the failure #104 exists to prevent, arriving
by a different door.

Which reveals a boundary worth drawing. Recovery is instance policy; a feature plugin is
a per-person preference. A per-person switch that can silently change whether an account is
recoverable is the wrong shape. Either recovery reads only verified numbers regardless of the
plugin, or the plugin cannot be switched off for an account whose only route is a number —
the same lock, one level down.

Somebody with no verified number is unaffected, which is most people on the day this
lands, and the lock must stay shut for them. _accounts_needing_email() counts accounts with
nothing else; it grows a second clause rather than an assumption.

## Observation > multiple contacts plugin should in intrinsic link to the account, the primary contact can be > used as recovery route when sms notifications is written on the code ## What exists **The intrinsic link is already there, and it is worth being precise about its shape.** A `PhoneNumber` has an `owner` — the account — and a `holder`, which is a generic relation to either that person's `Profile` or a `Contact` at a company. So every row belongs to an account, and the account's *own* numbers are the ones whose holder is their profile. Exactly one of those is `is_primary`, enforced by a partial unique index rather than by a form. Numbers are unique across the instance, which was a deliberate choice made against the alternative, and they are **unverified**, which was a consequence of having no channel to verify them over. **The recovery machinery is already written to accept a new route.** `recovery_routes()` returns a list, `refuse_switching_off()` reads it, and the docstring says a route that lands is added there and *"the lock below opens by itself"*. Nothing needs redesigning to admit one — which is why the interesting work is entirely in the two prerequisites. ## What this asks for The account's primary number as a way back in, joining email and the passkey. ## Worth being careful about **The choice made in #90 turns out to have been the right one for this.** Instance-wide uniqueness was argued about and taken with its disclosure named; a number that identifies an account for recovery *has* to be unique, so the rule that looked like tidiness is now load-bearing. Worth recording that it was not chosen for this reason and holds anyway. **Primary is a property of a holder, not of an account.** Each contact has a primary number too, and a recovery lookup that reads `is_primary` without filtering the holder would happily find a recruiter's mobile. Anything here filters on the profile first. **The number that gets somebody back in should probably not be the one on their CV.** The primary is what a document prints — a recruiter dials it. Making the same row the recovery route couples "the number I publish" to "the number that proves I am me", and somebody who changes the number on their CV changes their recovery route without being told. Either the recovery number is chosen separately, or changing the primary says out loud what else it changes. **Switching the plugin off must not remove a way back in.** #90's rule is that off shows the primary and keeps the rest; if the primary is a recovery route, off has to keep it *working* as one while hiding the others. And the account-recovery interlock (#104, #103) must count it regardless of any per-person plugin state, or an administrator switching the feature off for somebody quietly removes their route — which is the failure #104 exists to prevent, arriving by a different door. **Which reveals a boundary worth drawing.** Recovery is instance policy; a feature plugin is a per-person preference. A per-person switch that can silently change whether an account is recoverable is the wrong shape. Either recovery reads only verified numbers regardless of the plugin, or the plugin cannot be switched off for an account whose only route is a number — the same lock, one level down. **Somebody with no verified number is unaffected**, which is most people on the day this lands, and the lock must stay shut for them. `_accounts_needing_email()` counts accounts with nothing else; it grows a second clause rather than an assumption.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:48:00 +00:00
Author
Owner

PhoneNumber.is_recovery, and on this release no number carries it anywhere — nothing
can confirm a number until an operator installs a gateway (#143), so the route exists and
refuses to pretend, the same shape the two prerequisites took.

The issue's own doubt, resolved the way it leaned. "The number that gets somebody back in
should probably not be the one on their CV" — it is not. A separate flag rather than a
sentence warning that changing the primary changes something else: the coupling is removed
instead of explained. One per account, by partial unique index, exactly as is_primary is.

Primary is a property of a holder, and this one is not. mine() filters on the holder
being the person's own profile, so a recruiter's switchboard cannot be nominated. clean()
refuses that and refuses an unconfirmed number, on the model rather than only the form,
because the API, a management command and a shell all reach it. Editing the digits takes the
nomination away with the confirmation it rested on, in the same save().

The boundary the issue asked to be drawn, drawn the first way it offered. Recovery reads
a confirmed number regardless of the plugin. A per-person switch that could silently decide
whether an account is recoverable is the wrong shape — an administrator switching several
telephone numbers
off for somebody would otherwise remove their way back in, which is the
failure #104 exists to prevent arriving through a different door. The plugin still governs
what is shown and used; two tests hold both halves.

accounts_needing_email() grew a clause rather than an assumption, and needs both: a
nominated confirmed number and a gateway that can reach one. Somebody with no number is
unaffected and the lock stays shut for them, which is every account on every instance today.

#90's instance-wide uniqueness holds. Worth recording as the issue asked: it was argued
for on other grounds and turns out to be load-bearing here, since a number that identifies an
account for recovery has to be unique.

An archive cannot carry a nomination in — the importer drops it with the confirmation it
depends on. FORMAT_VERSION is 7. The control appears in the interface only where the
instance could confirm a number at all, because a control nobody can use beside a promise
nobody can keep is worse than no control.

19 tests in tests/test_recovery_number.py, a wiki section, and seven strings in all 39
European catalogues.

Shipped in ba4c3f9 on 0.3.0, with main kept level.

`PhoneNumber.is_recovery`, and on this release no number carries it anywhere — nothing can confirm a number until an operator installs a gateway (#143), so the route exists and refuses to pretend, the same shape the two prerequisites took. **The issue's own doubt, resolved the way it leaned.** "The number that gets somebody back in should probably not be the one on their CV" — it is not. A separate flag rather than a sentence warning that changing the primary changes something else: the coupling is removed instead of explained. One per account, by partial unique index, exactly as `is_primary` is. **Primary is a property of a holder, and this one is not.** `mine()` filters on the holder being the person's own profile, so a recruiter's switchboard cannot be nominated. `clean()` refuses that and refuses an unconfirmed number, on the model rather than only the form, because the API, a management command and a shell all reach it. Editing the digits takes the nomination away with the confirmation it rested on, in the same `save()`. **The boundary the issue asked to be drawn, drawn the first way it offered.** Recovery reads a confirmed number *regardless of the plugin*. A per-person switch that could silently decide whether an account is recoverable is the wrong shape — an administrator switching *several telephone numbers* off for somebody would otherwise remove their way back in, which is the failure #104 exists to prevent arriving through a different door. The plugin still governs what is shown and used; two tests hold both halves. **`accounts_needing_email()` grew a clause rather than an assumption**, and needs both: a nominated confirmed number **and** a gateway that can reach one. Somebody with no number is unaffected and the lock stays shut for them, which is every account on every instance today. **#90's instance-wide uniqueness holds.** Worth recording as the issue asked: it was argued for on other grounds and turns out to be load-bearing here, since a number that identifies an account for recovery has to be unique. An archive cannot carry a nomination in — the importer drops it with the confirmation it depends on. `FORMAT_VERSION` is 7. The control appears in the interface only where the instance could confirm a number at all, because a control nobody can use beside a promise nobody can keep is worse than no control. 19 tests in `tests/test_recovery_number.py`, a wiki section, and seven strings in all 39 European catalogues. Shipped in `ba4c3f9` on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#144
No description provided.