A customer can favorite and unfavorite an item. When a favorited item sells to someone else, a customer who has opted in to email is notified that it is no longer available. A customer who has not opted in is offered the opt-in, with the reason stated.
Scope
New table linking customers to items
Favorite/unfavorite from the storefront
A hook in the sale path that notifies favoriters when an item becomes sold
Consent-prompt UX for customers who have not opted in to marketing email
Why the consent prompt needs care
The existing GDPR handling is deliberate: marketing consent is never pre-checked, is timestamped with the exact consent text shown, and is separately revocable. This feature asks a customer to opt in for a specific reason — "so we can tell you if this sells" — which is arguably a different purpose from the existing marketing consent.
That needs deciding rather than assuming: is this the same consent flag, or a second, narrower one? Bundling a transactional notification into a marketing opt-in is the kind of thing that undermines the clean consent record already in place.
Design questions to resolve before implementing
Same marketing_consent flag, or a separate notification preference?
Does the notification fire when the favoriter themselves buys the item? (It should not.)
Does it fire on a demo-mode purchase, and on an admin "Mark Sold"?
Should favoriting require an account, or work anonymously until checkout the way add-to-cart does?
What happens to favorites when an item is deleted by an admin?
Is the email one-per-item, or batched?
Related
Filtering the storefront by favorites (#35) depends on this.
Split out of #31 (item 4).
## Goal
A customer can favorite and unfavorite an item. When a favorited item sells to someone else, a customer who has opted in to email is notified that it is no longer available. A customer who has not opted in is offered the opt-in, with the reason stated.
## Scope
- New table linking customers to items
- Favorite/unfavorite from the storefront
- A hook in the sale path that notifies favoriters when an item becomes `sold`
- Consent-prompt UX for customers who have not opted in to marketing email
## Why the consent prompt needs care
The existing GDPR handling is deliberate: marketing consent is never pre-checked, is timestamped with the exact consent text shown, and is separately revocable. This feature asks a customer to opt in for a *specific* reason — "so we can tell you if this sells" — which is arguably a different purpose from the existing marketing consent.
That needs deciding rather than assuming: is this the same consent flag, or a second, narrower one? Bundling a transactional notification into a marketing opt-in is the kind of thing that undermines the clean consent record already in place.
## Design questions to resolve before implementing
- Same `marketing_consent` flag, or a separate notification preference?
- Does the notification fire when the favoriter themselves buys the item? (It should not.)
- Does it fire on a demo-mode purchase, and on an admin "Mark Sold"?
- Should favoriting require an account, or work anonymously until checkout the way add-to-cart does?
- What happens to favorites when an item is deleted by an admin?
- Is the email one-per-item, or batched?
## Related
Filtering the storefront by favorites (#35) depends on this.
Branch feature/favorites, one commit, not yet pushed. Includes a migration, applied automatically on container start.
Decisions taken
Question
Answer
What consent are we asking for
A separate, narrower one — favorite_alerts, recorded with its own flag, timestamp, and the exact wording shown
What counts as "no longer available"
Sold only, to someone else — via checkout or an admin marking it sold
Signed-out visitors
Prompted to sign in, then the favorite completes
Why a separate consent
Being told that a specific item you asked about has gone is a narrower thing than agreeing to marketing. Reusing marketing_consent would have left marketing_consent_text no longer describing what the customer actually agreed to, and would sign someone up for campaigns when all they wanted was to hear about one table. Accepting item alerts leaves marketing_consent untouched, and there is an integration test asserting exactly that.
Why not notify on reserve
Reservations expire and get released, so a "this has gone" email would frequently be about an item that is still for sale. A last-chance warning that turns out to be wrong is worse than silence.
Why the buyer is excluded
Emailing a customer to say the item they just bought is unavailable reads as a bug. Their own purchase never triggers a notification to them, though other favoriters are still told.
Notification timing
completeCheckout now returns the sold item ids and the buyer, so its three call sites — PayPal capture, demo purchase, and the PayPal webhook — notify afterCOMMIT. Sending inside the transaction would email people about a sale that then rolled back, and would hold a database transaction open for the duration of SMTP. Each message is sent independently, so one bad address cannot stop the rest, and the sale has already succeeded regardless of whether mail goes out.
Disabled accounts are excluded, honouring the rule established in #33.
The prompt
It appears only after a customer has actually favorited something, so the reason for asking is concrete rather than an abstract marketing ask. It states plainly that it is separate from marketing email, and declining still keeps the favorite. The preference is manageable afterwards from the account page, alongside but distinct from the marketing toggle.
A latent bug fixed alongside
While the session is still resolving, customer is null for a signed-in visitor too. Clicking Add to Cart in that window prompted an already-signed-in customer to sign in again. Both that button and the new heart now ignore clicks until the session has resolved, and show as loading meanwhile. The favorites e2e caught this because it navigates and clicks immediately; a human would rarely be fast enough to hit it, but it was real.
Verification
Suite
Result
Backend unit
52 passed
Backend integration
121 passed (17 new)
Frontend e2e
63 passed (6 new), stable over two consecutive runs
tsc --noEmit backend / frontend
clean
npm run build
clean
Integration coverage includes idempotent favoriting, per-customer isolation, the consent being independent of marketing, and every notification rule: opted-in favoriters notified, non-opted-in not, the buyer excluded, multiple favoriters each notified, admin mark-sold notifying, and disabled accounts skipped.
Next
#35 (filter the storefront by favorites) is now unblocked — the favorites table and the shared FavoritesContext it needs both exist.
## Implemented
Branch `feature/favorites`, one commit, not yet pushed. Includes a migration, applied automatically on container start.
## Decisions taken
| Question | Answer |
| --- | --- |
| What consent are we asking for | **A separate, narrower one** — `favorite_alerts`, recorded with its own flag, timestamp, and the exact wording shown |
| What counts as "no longer available" | **Sold only, to someone else** — via checkout or an admin marking it sold |
| Signed-out visitors | **Prompted to sign in**, then the favorite completes |
### Why a separate consent
Being told that a specific item you asked about has gone is a narrower thing than agreeing to marketing. Reusing `marketing_consent` would have left `marketing_consent_text` no longer describing what the customer actually agreed to, and would sign someone up for campaigns when all they wanted was to hear about one table. Accepting item alerts leaves `marketing_consent` untouched, and there is an integration test asserting exactly that.
### Why not notify on reserve
Reservations expire and get released, so a "this has gone" email would frequently be about an item that is still for sale. A last-chance warning that turns out to be wrong is worse than silence.
### Why the buyer is excluded
Emailing a customer to say the item they just bought is unavailable reads as a bug. Their own purchase never triggers a notification to them, though other favoriters are still told.
## Notification timing
`completeCheckout` now returns the sold item ids and the buyer, so its three call sites — PayPal capture, demo purchase, and the PayPal webhook — notify **after** `COMMIT`. Sending inside the transaction would email people about a sale that then rolled back, and would hold a database transaction open for the duration of SMTP. Each message is sent independently, so one bad address cannot stop the rest, and the sale has already succeeded regardless of whether mail goes out.
Disabled accounts are excluded, honouring the rule established in #33.
## The prompt
It appears only after a customer has actually favorited something, so the reason for asking is concrete rather than an abstract marketing ask. It states plainly that it is separate from marketing email, and declining still keeps the favorite. The preference is manageable afterwards from the account page, alongside but distinct from the marketing toggle.
## A latent bug fixed alongside
While the session is still resolving, `customer` is null for a signed-in visitor too. Clicking **Add to Cart** in that window prompted an already-signed-in customer to sign in again. Both that button and the new heart now ignore clicks until the session has resolved, and show as loading meanwhile. The favorites e2e caught this because it navigates and clicks immediately; a human would rarely be fast enough to hit it, but it was real.
## Verification
| Suite | Result |
| --- | --- |
| Backend unit | 52 passed |
| Backend integration | 121 passed (17 new) |
| Frontend e2e | 63 passed (6 new), stable over two consecutive runs |
| `tsc --noEmit` backend / frontend | clean |
| `npm run build` | clean |
Integration coverage includes idempotent favoriting, per-customer isolation, the consent being independent of marketing, and every notification rule: opted-in favoriters notified, non-opted-in not, the buyer excluded, multiple favoriters each notified, admin mark-sold notifying, and disabled accounts skipped.
## Next
#35 (filter the storefront by favorites) is now unblocked — the `favorites` table and the shared `FavoritesContext` it needs both exist.
Five of the six were already built as specified. One was a genuine gap and is now done.
Question
Answer
Status
Same marketing_consent, or separate?
Separate notification preference
Already built — favorite_alerts with its own flag, timestamp and consent text, with a test asserting it does not touch marketing_consent
Fire when the favoriter buys it themselves?
It should not
Already built — the buyer is excluded, other favoriters are still told
Fire on demo purchase and admin Mark Sold?
Both
Already built — demo goes through completeCheckout, Mark Sold is wired directly, both tested
Favoriting anonymous, or require an account?
Require an account
Already built — inline sign-in prompt, and requireCustomer server-side
One email per item, or batched?
One per item
Already built
What happens when an admin deletes an item?
Email that it is no longer available, if opted in
Was missing — now implemented
The gap
favorites.item_id is ON DELETE CASCADE, so deleting an item took its favorites with it and nobody was told. Recipients are now collected before the delete — afterwards there is nobody left to look up — and the mail is sent only once the delete has succeeded, so nobody hears about a withdrawal that did not happen.
Two judgement calls in that, worth flagging in case either is wrong:
Items that had already sold are excluded. Their favoriters were told at the point of sale, and a second message about the same item reads as a duplicate rather than news. Deleting a sold item is therefore silent.
The wording differs from the sale email — "withdrawn and is no longer available" rather than "sold". The customer did not lose out to another buyer, and saying they had would be untrue.
Disabled accounts are excluded here too, consistent with everything else.
Verification
Suite
Result
Backend unit
52 passed
Backend integration
126 passed (5 new for withdrawal)
Frontend e2e
63 passed, two consecutive clean runs
tsc --noEmit backend / frontend
clean
New coverage: opted-in favoriters told on delete, non-opted-in not told, an already-sold item not producing a second message, disabled accounts skipped, and deletion still working when nobody favorited the item.
One note on the run: a single storefront filter test failed once during the sweep and then passed 36/36 on a focused repeat and across two further full runs. I could not reproduce it and do not believe it relates to this change, but recording it rather than quietly discarding it.
## Design answers checked against the implementation
Five of the six were already built as specified. One was a genuine gap and is now done.
| Question | Answer | Status |
| --- | --- | --- |
| Same `marketing_consent`, or separate? | Separate notification preference | Already built — `favorite_alerts` with its own flag, timestamp and consent text, with a test asserting it does not touch `marketing_consent` |
| Fire when the favoriter buys it themselves? | It should not | Already built — the buyer is excluded, other favoriters are still told |
| Fire on demo purchase and admin Mark Sold? | Both | Already built — demo goes through `completeCheckout`, Mark Sold is wired directly, both tested |
| Favoriting anonymous, or require an account? | Require an account | Already built — inline sign-in prompt, and `requireCustomer` server-side |
| One email per item, or batched? | One per item | Already built |
| **What happens when an admin deletes an item?** | **Email that it is no longer available, if opted in** | **Was missing — now implemented** |
## The gap
`favorites.item_id` is `ON DELETE CASCADE`, so deleting an item took its favorites with it and nobody was told. Recipients are now collected **before** the delete — afterwards there is nobody left to look up — and the mail is sent only once the delete has succeeded, so nobody hears about a withdrawal that did not happen.
Two judgement calls in that, worth flagging in case either is wrong:
- **Items that had already sold are excluded.** Their favoriters were told at the point of sale, and a second message about the same item reads as a duplicate rather than news. Deleting a sold item is therefore silent.
- **The wording differs from the sale email** — "withdrawn and is no longer available" rather than "sold". The customer did not lose out to another buyer, and saying they had would be untrue.
Disabled accounts are excluded here too, consistent with everything else.
## Verification
| Suite | Result |
| --- | --- |
| Backend unit | 52 passed |
| Backend integration | 126 passed (5 new for withdrawal) |
| Frontend e2e | 63 passed, two consecutive clean runs |
| `tsc --noEmit` backend / frontend | clean |
New coverage: opted-in favoriters told on delete, non-opted-in not told, an already-sold item not producing a second message, disabled accounts skipped, and deletion still working when nobody favorited the item.
One note on the run: a single storefront filter test failed once during the sweep and then passed 36/36 on a focused repeat and across two further full runs. I could not reproduce it and do not believe it relates to this change, but recording it rather than quietly discarding it.
Reviewed in QA first: the heart prompts a signed-out visitor to register and applies the favorite afterwards, the alert opt-in is offered separately from marketing consent, and the account page shows the two switches independently. All passed.
One caveat worth leaving on the record: the emails themselves could not be exercised in QA, which runs without SMTP by design. Production is the first place notifyFavoritersOfSale and notifyFavoritersOfRemoval actually send. Sends are caught per-recipient and only logged, so a misconfiguration would leave sales working normally while nobody is notified — the failure would be silent. Worth one deliberate smoke test in production: favorite an item from a test account with alerts on, mark it sold in admin, confirm the mail arrives, and confirm the buyer is not mailed about their own purchase.
The schema change (add-favorites) is additive and idempotent, so the previous image runs unchanged against the migrated schema if a rollback is ever needed.
**Released to production.**
Reviewed in QA first: the heart prompts a signed-out visitor to register and applies the favorite afterwards, the alert opt-in is offered separately from marketing consent, and the account page shows the two switches independently. All passed.
One caveat worth leaving on the record: **the emails themselves could not be exercised in QA**, which runs without SMTP by design. Production is the first place `notifyFavoritersOfSale` and `notifyFavoritersOfRemoval` actually send. Sends are caught per-recipient and only logged, so a misconfiguration would leave sales working normally while nobody is notified — the failure would be silent. Worth one deliberate smoke test in production: favorite an item from a test account with alerts on, mark it sold in admin, confirm the mail arrives, and confirm the buyer is *not* mailed about their own purchase.
The schema change (`add-favorites`) is additive and idempotent, so the previous image runs unchanged against the migrated schema if a rollback is ever needed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Split out of #31 (item 4).
Goal
A customer can favorite and unfavorite an item. When a favorited item sells to someone else, a customer who has opted in to email is notified that it is no longer available. A customer who has not opted in is offered the opt-in, with the reason stated.
Scope
soldWhy the consent prompt needs care
The existing GDPR handling is deliberate: marketing consent is never pre-checked, is timestamped with the exact consent text shown, and is separately revocable. This feature asks a customer to opt in for a specific reason — "so we can tell you if this sells" — which is arguably a different purpose from the existing marketing consent.
That needs deciding rather than assuming: is this the same consent flag, or a second, narrower one? Bundling a transactional notification into a marketing opt-in is the kind of thing that undermines the clean consent record already in place.
Design questions to resolve before implementing
marketing_consentflag, or a separate notification preference?Related
Filtering the storefront by favorites (#35) depends on this.
Implemented
Branch
feature/favorites, one commit, not yet pushed. Includes a migration, applied automatically on container start.Decisions taken
favorite_alerts, recorded with its own flag, timestamp, and the exact wording shownWhy a separate consent
Being told that a specific item you asked about has gone is a narrower thing than agreeing to marketing. Reusing
marketing_consentwould have leftmarketing_consent_textno longer describing what the customer actually agreed to, and would sign someone up for campaigns when all they wanted was to hear about one table. Accepting item alerts leavesmarketing_consentuntouched, and there is an integration test asserting exactly that.Why not notify on reserve
Reservations expire and get released, so a "this has gone" email would frequently be about an item that is still for sale. A last-chance warning that turns out to be wrong is worse than silence.
Why the buyer is excluded
Emailing a customer to say the item they just bought is unavailable reads as a bug. Their own purchase never triggers a notification to them, though other favoriters are still told.
Notification timing
completeCheckoutnow returns the sold item ids and the buyer, so its three call sites — PayPal capture, demo purchase, and the PayPal webhook — notify afterCOMMIT. Sending inside the transaction would email people about a sale that then rolled back, and would hold a database transaction open for the duration of SMTP. Each message is sent independently, so one bad address cannot stop the rest, and the sale has already succeeded regardless of whether mail goes out.Disabled accounts are excluded, honouring the rule established in #33.
The prompt
It appears only after a customer has actually favorited something, so the reason for asking is concrete rather than an abstract marketing ask. It states plainly that it is separate from marketing email, and declining still keeps the favorite. The preference is manageable afterwards from the account page, alongside but distinct from the marketing toggle.
A latent bug fixed alongside
While the session is still resolving,
customeris null for a signed-in visitor too. Clicking Add to Cart in that window prompted an already-signed-in customer to sign in again. Both that button and the new heart now ignore clicks until the session has resolved, and show as loading meanwhile. The favorites e2e caught this because it navigates and clicks immediately; a human would rarely be fast enough to hit it, but it was real.Verification
tsc --noEmitbackend / frontendnpm run buildIntegration coverage includes idempotent favoriting, per-customer isolation, the consent being independent of marketing, and every notification rule: opted-in favoriters notified, non-opted-in not, the buyer excluded, multiple favoriters each notified, admin mark-sold notifying, and disabled accounts skipped.
Next
#35 (filter the storefront by favorites) is now unblocked — the
favoritestable and the sharedFavoritesContextit needs both exist.Design answers checked against the implementation
Five of the six were already built as specified. One was a genuine gap and is now done.
marketing_consent, or separate?favorite_alertswith its own flag, timestamp and consent text, with a test asserting it does not touchmarketing_consentcompleteCheckout, Mark Sold is wired directly, both testedrequireCustomerserver-sideThe gap
favorites.item_idisON DELETE CASCADE, so deleting an item took its favorites with it and nobody was told. Recipients are now collected before the delete — afterwards there is nobody left to look up — and the mail is sent only once the delete has succeeded, so nobody hears about a withdrawal that did not happen.Two judgement calls in that, worth flagging in case either is wrong:
Disabled accounts are excluded here too, consistent with everything else.
Verification
tsc --noEmitbackend / frontendNew coverage: opted-in favoriters told on delete, non-opted-in not told, an already-sold item not producing a second message, disabled accounts skipped, and deletion still working when nobody favorited the item.
One note on the run: a single storefront filter test failed once during the sweep and then passed 36/36 on a focused repeat and across two further full runs. I could not reproduce it and do not believe it relates to this change, but recording it rather than quietly discarding it.
Released to production.
Reviewed in QA first: the heart prompts a signed-out visitor to register and applies the favorite afterwards, the alert opt-in is offered separately from marketing consent, and the account page shows the two switches independently. All passed.
One caveat worth leaving on the record: the emails themselves could not be exercised in QA, which runs without SMTP by design. Production is the first place
notifyFavoritersOfSaleandnotifyFavoritersOfRemovalactually send. Sends are caught per-recipient and only logged, so a misconfiguration would leave sales working normally while nobody is notified — the failure would be silent. Worth one deliberate smoke test in production: favorite an item from a test account with alerts on, mark it sold in admin, confirm the mail arrives, and confirm the buyer is not mailed about their own purchase.The schema change (
add-favorites) is additive and idempotent, so the previous image runs unchanged against the migrated schema if a rollback is ever needed.