Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
40 changes: 40 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2089,3 +2089,43 @@ promising shape is to give `mutation:audit-equivalents` a way to attempt a
distinguishing input for each entry — or, failing that, an explicit re-audit
stamp so an entry has to be re-confirmed after the file it lives in changes
shape, instead of resting on a proof nobody has re-read since it was written.

## Deleting your own contact record also deletes your promotions opt-out

_Origin: found while writing the `attendees.asking-to-be-left-alone` story (the
reader-side unsubscribe journey)._

The "Delete my data" press on `/unsubscribe` calls `forgetContact`
(`src/shared/db/contact-preferences.ts`), which deletes the whole
`contact_preferences` row — including the `unsubscribed` flag. The bookings that
carry the person's (encrypted) address survive, so someone who first
unsubscribed and then asked to be forgotten becomes reachable by the next
promotion again: the send path (`getUnsubscribedHashSet`) no longer finds their
hash.

The story states what the product does today — "the site keeps no record under
that code at all — including the choices they had made on this page" — rather
than softening it. Whether that is the right product behaviour is a real
question with pull in both directions: erasure means erasure, but suppression
lists exist precisely because a marketing opt-out has to survive other
deletions. Settling it means choosing between (a) keeping a bare
`{hash, unsubscribed}` row behind on forget, (b) wording the page's
`unsubscribe.forget_explainer` to say promotions may resume, or (c) leaving it
as is, deliberately. Starting points: `forgetContact`, the forget branch in
`src/features/public/unsubscribe.ts`, and the story's
`@rule:attendees.the-record-under-their-code-can-be-deleted`.

## Move the unsubscribe flash wording into the message catalog

_Origin: the same story migration._

The three flash messages the `/unsubscribe` POST sends back ("You've
unsubscribed from our marketing emails.", "You've resubscribed to our marketing
emails.", "Your contact record has been deleted.", plus "That link is invalid.")
are string literals in `src/features/public/unsubscribe.ts`. The `i18n-coverage`
gate only scans templates, so nothing flags them, but they are user-facing copy
and belong in `src/locales/en/unsubscribe.json` like the rest of the page's
words — then the story steps and the direct pins in
`test/integration/routes/unsubscribe.test.ts` can assert them through `t()`
instead of pinning copied wording. Out of scope for the migration, which does
not touch `src/`.
85 changes: 85 additions & 0 deletions specs/attendees/asking-to-be-left-alone.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@story:attendees.asking-to-be-left-alone
@owner:attendees @risk:high
@actor:customer
@edition:managed @edition:self-hosted
@surface:public
Feature: Somebody the site emails can ask to be left alone
Every promotion the site sends ends with a link to the reader's own
choices page. That page is found by a one-way code, never by their
address, and it is theirs alone: one press stops promotions, a later
press lets them back in, and a third deletes the record the site
keeps under their code. A link that has lost its code explains
itself and offers nothing to press, so a broken link can never
change anything.

@rule:attendees.every-promotion-carries-the-way-out
Rule: Every promotion carries the way out
The link at the bottom of a promotion leads to the reader's own
choices page, which tells them where they stand before they press
anything.

@case:leave-alone.the-link-leads-to-their-own-page
Scenario: A reader follows the link at the bottom of a promotion
Given the owner has an email provider of their own
And 2 people have booked onto "the Gig"
And the owner has sent a promotion to "the Gig" saying "Half price Friday."
When one of them follows the choices link in their copy
Then they land on their own choices page, still subscribed to promotions

@rule:attendees.asking-not-to-hear-is-one-press
Rule: Asking not to hear is one press, and it is kept
Pressing the button on their own page is all it takes. The reader
is told it worked, the site counts them as having asked not to
hear, and the page they are left on offers the way back in rather
than a dead end.

@case:leave-alone.one-press-stops-the-promotions
Scenario: One press stops the promotions
Given the owner has an email provider of their own
And 2 people have booked onto "the Gig"
And the owner has sent a promotion to "the Gig" saying "Half price Friday."
And one of them follows the choices link in their copy
When they ask to stop hearing about promotions
Then they are told they have unsubscribed
And the site counts them as having asked not to hear
And their page offers them a way back in

@rule:attendees.a-change-of-mind-puts-them-back
Rule: A change of mind puts them back
Someone who asked not to hear can press the way back in on the
same page. The site stops counting them as having asked not to
hear, so promotions may reach them again.

@case:leave-alone.a-change-of-mind
Scenario: They change their mind and come back
Given somebody has asked not to hear about promotions
When they change their mind on their own choices page
Then they are told they have resubscribed
And the site counts them as hearing about promotions again

@rule:attendees.the-record-under-their-code-can-be-deleted
Rule: The record under their code can be deleted by them
The same page offers to delete the record the site keeps under
their one-way code. After the press the site keeps no record under
that code at all — including the choices they had made on this
page.

@case:leave-alone.deleting-their-own-record
Scenario: They delete the record the site keeps
Given somebody has asked not to hear about promotions
When they delete their data from their choices page
Then they are told their record was deleted
And the site keeps no record under their code
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

@rule:attendees.a-broken-link-changes-nothing
Rule: A broken link changes nothing
A choices link that has lost its code — cut short by an email
program, or mistyped — explains itself. The page offers nothing to
press, so nobody's choices can be changed by a link that names no
one.

@case:leave-alone.a-broken-link-explains-itself
Scenario: A link with no code explains itself
When somebody opens the choices page from a broken link
Then they are told the link is invalid
And the page offers them nothing to press
72 changes: 72 additions & 0 deletions specs/catalogue/choosing-a-bulk-action-for-a-group.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@story:catalogue.choosing-a-bulk-action-for-a-group
@owner:catalogue @risk:low
@actor:organiser
@edition:managed @edition:self-hosted
@surface:admin
Feature: The organiser chooses what to do with a group from a landing page
An organiser with a group of listings opens the group's bulk-actions
page to see what they can do with the whole group at once. The page
always offers a copy, and it offers taking the group off sale or
bringing it back only when the action would do something — taking off
sale needs at least one listing still on sale, and bringing back
waits until every listing is off sale. An empty group has nothing to
switch either way, so only the copy is offered.

@rule:catalogue.bulk-actions-landing-offers-copy-and-deactivate-when-active
Rule: A group with active listings offers copy and deactivate
When every listing in the group is on sale, the organiser can copy
the group or take it off sale. The reactivate action is not offered,
because there is nothing off sale to bring back.

@case:catalogue.bulk-actions-landing-all-active
Scenario: An all-active group offers copy and deactivate
Given the site has a group called "Workshops" with "Spring Workshop" on sale
When the organiser opens the bulk actions page for the "Workshops" group
Then the organiser is offered a way to copy the group
And the organiser is offered a way to take the group off sale
And the organiser is not offered a way to bring the group back on sale
And the page says it holds 1 listing

@rule:catalogue.bulk-actions-landing-offers-copy-and-reactivate-when-inactive
Rule: A group with no active listings offers copy and reactivate
When every listing in the group is off sale, the organiser can copy
the group or bring it back on sale. The deactivate action is not
offered, because there is nothing on sale to take off.

@case:catalogue.bulk-actions-landing-all-inactive
Scenario: An all-inactive group offers copy and reactivate
Given the site has a group called "Finished" with "Old Season" on sale
And "Old Season" is taken off sale
When the organiser opens the bulk actions page for the "Finished" group
Then the organiser is offered a way to copy the group
And the organiser is offered a way to bring the group back on sale
And the organiser is not offered a way to take the group off sale
And the page says it holds 1 listing

@rule:catalogue.bulk-actions-landing-mixed-and-empty-groups
Rule: A mixed group can still be taken off sale; an empty group offers only the copy
A group with some listings on sale and some off can still be copied
and taken off sale — the deactivate link needs only one listing
still on sale. Bringing the group back needs every listing to be off
sale, so a mixed group is not offered it. A group with no listings
has nothing to switch either way, so only the copy is offered.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

@case:catalogue.bulk-actions-landing-mixed
Scenario: A mixed group offers copy and deactivate but not reactivate
Given the site has a group called "Mixed" with "Active One" on sale
And the site has a group called "Mixed" with "Inactive One" on sale
And "Inactive One" is taken off sale
When the organiser opens the bulk actions page for the "Mixed" group
Then the organiser is offered a way to copy the group
And the organiser is offered a way to take the group off sale
And the organiser is not offered a way to bring the group back on sale
And the page says it holds 2 listings

@case:catalogue.bulk-actions-landing-empty
Scenario: An empty group offers only the copy
Given the site has a group called "Empty" with no listings
When the organiser opens the bulk actions page for the "Empty" group
Then the organiser is offered a way to copy the group
And the organiser is not offered a way to take the group off sale
And the organiser is not offered a way to bring the group back on sale
And the page says it holds 0 listings
31 changes: 30 additions & 1 deletion specs/catalogue/taking-a-group-off-sale.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
@actor:organiser
@edition:managed @edition:self-hosted
@surface:admin
Feature: The organiser takes a whole group of listings off sale at once
Feature: The organiser takes a whole group of listings off sale, and brings it back
An organiser with a group of listings — a season of workshops, a set
of rooms — can take every listing in the group off sale in one action,
so that none of them can be booked until they are brought back. The
organiser confirms by typing the group's name, because taking
something off sale affects every booking page, every payment, and
every page a buyer might reach. A wrong name leaves everything as it
was, and listings that belong to a different group are not touched.
Bringing the group back on sale works the same way: type the group's
name to confirm, and every listing in it goes back on sale together.

@rule:catalogue.deactivate-takes-every-member-off-sale
Rule: A confirmed deactivation takes every member off sale
Expand Down Expand Up @@ -44,6 +46,33 @@ Feature: The organiser takes a whole group of listings off sale at once
And the confirmation form says it will deactivate 1 active listing(s)
And every listing in the "Keep Active" group is still on sale

@rule:catalogue.reactivate-brings-every-member-back-on-sale
Rule: A confirmed reactivation brings every member back on sale
Bringing a group back works the same way as taking it off. The
organiser types the group's name to confirm, and every listing in
the group goes back on sale in one action. A wrong name is refused,
and every listing stays off sale.

@case:catalogue.reactivate-confirms-and-brings-every-member-back
Scenario: The organiser brings a group back on sale by typing its name
Given the site has a group called "Winter Season" with "January Show" on sale
And the site has a group called "Winter Season" with "February Show" on sale
And the organiser has taken the "Winter Season" group off sale
When the organiser brings the "Winter Season" group back on sale, typing its name to confirm
Then the organiser is sent to the "Winter Season" group's page
And the confirmation form says it will reactivate 2 listing(s)
And every listing in the "Winter Season" group is back on sale

@case:catalogue.reactivate-refuses-with-a-wrong-name
Scenario: The organiser types a name that does not match when bringing a group back
Given the site has a group called "Stay Off" with "Hidden Show" on sale
And the organiser has taken the "Stay Off" group off sale
When the organiser tries to bring the "Stay Off" group back on sale, typing "Wrong Name" instead
Then the organiser is told the group name does not match
And the organiser is still on the "Stay Off" group's reactivate form
And the confirmation form says it will reactivate 1 listing(s)
And every listing in the "Stay Off" group is still off sale

@rule:catalogue.deactivate-leaves-other-groups-alone
Rule: A deactivation does not touch listings outside the group
Taking a group off sale only affects the listings in that group.
Expand Down
Loading