fix(web): cancel-at-period-end button shows 'keep access until N/A' instead of the real end date (BUNYIP-330) #317

Closed
opened 2026-07-02 01:04:27 +02:00 by longjacksonle · 0 comments

Summary

On the member dashboard, the "Cancel Membership" control expands to a "Cancel at period end - keep access until N/A" button. When Stripe's current period end is not known, the label renders the literal string "N/A" instead of a real date, so the member cannot tell how long they keep access after cancelling. The same "N/A" fallback also appears in the "Next Billing" field of the membership card. We want the end date shown specifically, and a clearer fallback when it is genuinely unavailable.

Where

bunyip-web/src/handlers/dashboard.rs (member membership panel):

  • Line ~961-962: "Next Billing" value uses m.current_period_end.as_deref().map(fmt_date_iso).unwrap_or_else(|| "N/A".into()), then renders "Canceled - ends " (end) when cancelling, else (end).
  • Line ~972 + ~978: the cancel-at-period-end form button label is "Cancel at period end - keep access until " (end), reusing the same unwrap_or_else(|| "N/A") fallback.

Root cause

current_period_end is Option<DateTime<Utc>> on the domain model (crates/bunyip-domain/src/models/membership.rs:14) and Option<String> on the web API type (bunyip-web/src/api/types.rs:129). It is populated only from the Stripe subscription sync (crates/bunyip-domain/src/services/stripe.rs:686, driven by webhooks). When that value is None, the Maud template substitutes the literal "N/A".

current_period_end is None whenever:

  • A member has just subscribed and the customer.subscription.created/updated webhook has not landed yet.
  • Any sync gap leaves the membership row without a Stripe period.

So the "N/A" is the None-branch of the formatter, not a formatting bug in fmt_date_iso (which correctly renders %B %-d, %Y, e.g. "July 14, 2026", when a value is present).

Desired behavior / acceptance criteria

  • AC1: When current_period_end is present, the cancel-at-period-end button reads "Cancel at period end - keep access until <Month D, YYYY>" (it already formats correctly; verify it renders once a value exists).
  • AC2: When current_period_end is absent, replace the bare "N/A" with a clear phrase that does not imply an error, e.g. "keep access until the end of your current billing period" for the cancel button, and a comparable non-"N/A" fallback for the "Next Billing" field.
  • AC3: Apply the same treatment consistently to all three sites (Next Billing value, the "Canceled - ends" line, and the cancel button label) so no user-facing surface shows "N/A".
  • AC4: No behavioral change to the cancel/cancel-now/reactivate actions themselves; this is display-only.

Notes

Display-only, bunyip-web templating change. No migration, no API change. Relates to the BUNYIP-291 cancel-control work that introduced this combined cancel UI.

## Summary On the member dashboard, the "Cancel Membership" control expands to a "Cancel at period end - keep access until N/A" button. When Stripe's current period end is not known, the label renders the literal string "N/A" instead of a real date, so the member cannot tell how long they keep access after cancelling. The same "N/A" fallback also appears in the "Next Billing" field of the membership card. We want the end date shown specifically, and a clearer fallback when it is genuinely unavailable. ## Where `bunyip-web/src/handlers/dashboard.rs` (member membership panel): - Line ~961-962: "Next Billing" value uses `m.current_period_end.as_deref().map(fmt_date_iso).unwrap_or_else(|| "N/A".into())`, then renders `"Canceled - ends " (end)` when cancelling, else `(end)`. - Line ~972 + ~978: the cancel-at-period-end form button label is `"Cancel at period end - keep access until " (end)`, reusing the same `unwrap_or_else(|| "N/A")` fallback. ## Root cause `current_period_end` is `Option<DateTime<Utc>>` on the domain model (`crates/bunyip-domain/src/models/membership.rs:14`) and `Option<String>` on the web API type (`bunyip-web/src/api/types.rs:129`). It is populated only from the Stripe subscription sync (`crates/bunyip-domain/src/services/stripe.rs:686`, driven by webhooks). When that value is `None`, the Maud template substitutes the literal `"N/A"`. `current_period_end` is `None` whenever: - A member has just subscribed and the `customer.subscription.created/updated` webhook has not landed yet. - Any sync gap leaves the membership row without a Stripe period. So the "N/A" is the None-branch of the formatter, not a formatting bug in `fmt_date_iso` (which correctly renders `%B %-d, %Y`, e.g. "July 14, 2026", when a value is present). ## Desired behavior / acceptance criteria - AC1: When `current_period_end` is present, the cancel-at-period-end button reads "Cancel at period end - keep access until <Month D, YYYY>" (it already formats correctly; verify it renders once a value exists). - AC2: When `current_period_end` is absent, replace the bare "N/A" with a clear phrase that does not imply an error, e.g. "keep access until the end of your current billing period" for the cancel button, and a comparable non-"N/A" fallback for the "Next Billing" field. - AC3: Apply the same treatment consistently to all three sites (Next Billing value, the "Canceled - ends" line, and the cancel button label) so no user-facing surface shows "N/A". - AC4: No behavioral change to the cancel/cancel-now/reactivate actions themselves; this is display-only. ## Notes Display-only, `bunyip-web` templating change. No migration, no API change. Relates to the BUNYIP-291 cancel-control work that introduced this combined cancel UI.
Sign in to join this conversation.
No labels
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.

Dependencies

No dependencies set

Reference
psa-systems/bunyip#317
No description provided.