Skip to content

feat(billing): persist cancellation feedback and email the customer#1729

Open
giladresisi wants to merge 3 commits into
mainfrom
feat/cancellation-feedback
Open

feat(billing): persist cancellation feedback and email the customer#1729
giladresisi wants to merge 3 commits into
mainfrom
feat/cancellation-feedback

Conversation

@giladresisi

@giladresisi giladresisi commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Motivation

POST /billing/cancel sent an internal email to EMAIL_FROM_ADDRESS before even calling Stripe. That meant:

  • Internal inbox flooding — the email fired unconditionally: on reactivations (the same endpoint is a toggle) and even when the Stripe call subsequently failed.
  • No traceability — the customer's feedback existed only inside that email and was never stored anywhere queryable.
  • No customer confirmation — the person cancelling received nothing.

Changes

  • The controller now calls setToCancel first and only acts on a real cancellation (cancel_at set). Reactivation saves and sends nothing.
  • New SubscriptionCancellationFeedback table (purely additive schema change: new model + back-relations) persists the feedback per cancellation event, so history survives cancel/reactivate cycles.
  • The customer gets a confirmation email with the human-readable access-end date and a link back to the billing page. The internal email is removed entirely — realtime internal notification will be handled by a separate mechanism later.
  • New BillingCancelDto (@IsString(), @IsDefined()): the previously unvalidated inline body now rejects a missing feedback, while the empty string stays valid because the frontend reactivate path posts feedback: ''.
  • Email failure cannot fail the request; the { id, cancel_at } response shape is unchanged and no frontend changes are needed. The chatbase-refund email in the same controller is untouched.

Deployment

Requires a DB schema update (pnpm run prisma-db-push) before or with the deploy — this repo uses prisma db push, so there is no migration file. The change is purely additive (one new table SubscriptionCancellationFeedback + back-relations on Organization/User): no existing tables/columns are altered, no data backfill needed, and old backend code keeps working against the pushed schema, so the usual schema-push-first deploy order is safe.

Testing

  • pnpm run prisma-generate, pnpm run prisma-db-push (clean, no drop warnings), and pnpm run build:backend all pass.
  • Synthetic e2e against the local stack (fresh Stripe test-mode customer + subscription, self-cleaning):
    • missing feedback → 400, no row saved
    • cancel with real feedback → { id, cancel_at } returned, feedback row saved with correct org/user, exactly one Temporal send_email signal addressed to the customer (payload inspected: subject, date, billing link) and none to EMAIL_FROM_ADDRESS
    • reactivate with feedback: ''cancel_at undefined, no new row, no email, Stripe subscription restored to active/non-cancelling
  • Cancel/reactivate flows also verified manually through the billing UI with a test-card subscription.

🤖 Generated with Claude Code

POST /billing/cancel used to fire an internal email to EMAIL_FROM_ADDRESS
before even calling Stripe — flooding the team inbox (it fired on
reactivations and on Stripe failures too), throwing the customer's feedback
away after the email, and giving the customer no confirmation at all.

Now the controller calls setToCancel first and only acts on a real
cancellation (cancel_at set): the feedback is persisted in a new
SubscriptionCancellationFeedback table so it stays queryable/traceable
(history survives cancel/reactivate cycles), and the customer gets a
confirmation email with the access-end date and a link to the billing page.
Reactivation saves and sends nothing. The internal email is removed;
realtime internal notification will be handled by a separate mechanism.

- Purely additive schema change (new model + back-relations); db push clean
- New BillingCancelDto: @IsString() with empty string allowed — the
  frontend reactivate path posts feedback: ''
- Email failure cannot fail the request; response shape { id, cancel_at }
  unchanged, no frontend changes

Testing: prisma-generate / prisma-db-push / build:backend all pass.
Synthetic e2e against the local stack (fresh Stripe test-mode
customer+subscription, self-cleaning): missing feedback rejected with 400
and no row; cancel saves the row and emits exactly one Temporal send_email
signal addressed to the customer (none to EMAIL_FROM_ADDRESS), body verified;
reactivate returns cancel_at undefined with no new row/email and the Stripe
subscription restored. Cancel/reactivate also verified manually through the
billing UI with a test-card subscription.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@postiz-contribution postiz-contribution Bot added the contribution:approved Approved contributor label Jul 15, 2026
@postiz-contribution

Copy link
Copy Markdown

Contribution-checker quality warning
Heuristic score: 6/100 (low). This is a non-blocking warning surfaced by the project's quality settings.

Heuristics that flagged:

  • Excessive inline code references: 20 inline refs (>3)
  • PR doesn't use the repo's PR template: 5 required checkbox items missing (max 1, match ≥80%)
  • Body adds too many extra headers beyond the template: 3 extra headers (>1)
  • AI watermark phrase: Matched: "Generated with Claude Code"
  • Commit message too long: Longest: 1750 chars

If this is a genuine contribution, please add detail to your PR description and tighten the diff scope before reviewers look at it.

@postiz-agent

postiz-agent Bot commented Jul 15, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Comment on lines +123 to +128
if (result.cancel_at) {
await this._subscriptionService.saveCancellationFeedback(
org.id,
user.id,
body.feedback
);

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in followup commit

egelhaus and others added 2 commits July 15, 2026 08:20
Addresses the review comment on #1729: by the time we save the feedback the
Stripe cancellation has already succeeded, so a transient DB error would
return a 500 while the subscription is in fact cancelled, leaving the UI
inconsistent. Log the failure and still return { id, cancel_at }, matching
how the confirmation email is already guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution:approved Approved contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants