Skip to content

Persist subscription lapse reasons for #344 - #486

Merged
thomasluizon merged 8 commits into
mainfrom
feature/ticket-344-lapse-reason
Aug 23, 2026
Merged

Persist subscription lapse reasons for #344#486
thomasluizon merged 8 commits into
mainfrom
feature/ticket-344-lapse-reason

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Fixes #344.

Summary

  • Persists canceled, payment_failed, or expired when Stripe or Play entitlement ends, together with SubscriptionEndedAtUtc.
  • Records Stripe invoice.payment_failed before access is removed and clears the pending reason after successful renewal.
  • Keeps pending lapse state owned by the provider that currently supplies entitlement.
  • Rejects older Stripe lifecycle events after a newer outcome by persisting the latest accepted Stripe event time.
  • Appends nullable LapseReason and SubscriptionEndedAtUtc fields to the subscription status response.
  • Adds the EF migrations plus domain, status query, Stripe webhook, and Play notification coverage.

External interface evidence

Stripe.net 52.2.0 is the installed package. Its generated XML documents InvoiceParent.SubscriptionDetails, InvoiceParentSubscriptionDetails.SubscriptionId, Subscription.CancellationDetails, SubscriptionCancellationDetails.Reason, and Event.Created. Event.Created is documented as seconds since the Unix epoch. The reason values used here are cancellation_requested, payment_failed, and payment_disputed. Reproduce with dotnet list src/Orbit.Application/Orbit.Application.csproj package and inspect the installed stripe.net 52.2.0 Stripe.net.xml members.

The redacted invoice shape read by the handler is:

{
  "object": "invoice",
  "parent": {
    "type": "subscription_details",
    "subscription_details": {
      "subscription": "sub_redacted"
    }
  }
}

The redacted subscription fields read by the handler are:

{
  "object": "subscription",
  "status": "canceled",
  "cancellation_details": {
    "reason": "cancellation_requested"
  }
}

The redacted event ordering field read by the handler is:

{
  "object": "event",
  "created": 1787457600
}

Google documents Play RTDN subscription notification type 3 as canceled, type 5 as account hold, and type 13 as expired at https://developer.android.com/google/play/billing/rtdn-reference. The handler verifies the purchase through the existing provider API path before applying these values.

Verification

  • dotnet build Orbit.slnx completed with 0 errors.
  • dotnet test passed 5,789 tests on current main.

Assumptions

  • expired is the provider neutral value for a subscription that reached its end. I rejected exposing raw Stripe or Play status strings because the client contract must be common across providers.
  • SubscriptionEndedAtUtc records webhook processing time when entitlement is cleared. I rejected storing a user local calendar date because the API carries a precise ending moment and clients localize it.
  • Play cancellation type 3 preserves an earlier payment failure reason, and expiration type 13 preserves any earlier specific reason. I rejected overwriting actionable history with later generic lifecycle notifications.

@thomasluizon

Copy link
Copy Markdown
Owner Author

I will add a lapse reason value and ending date to User, thread explicit reasons through both cancellation methods, split Stripe terminal statuses, mirror Play cancellation, and record invoice.payment_failed. Persistence configuration and an EF migration will retain the fields. SubscriptionStatusResponse and its query will append nullable values. Focused domain, webhook, and query tests will prove distinct reasons and null behavior. This keeps historical provider state on User because provider ids are deliberately cleared. I rejected deriving lapse state from live Stripe or Play records because the status endpoint must still answer after those ids are gone.

pullfrog[bot]
pullfrog Bot previously approved these changes Aug 23, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the initial scaffold for the planned subscription lapse reason work.

  • Empty scaffold: The sole commit changes no files, so there is no implementation, persistence, migration, API contract, or test surface to assess yet.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

Two billing state paths can persist a lapse reason that does not describe the user's current or latest entitlement and must be corrected before merge.

Reviewed changes Reviewed the implementation added since the prior Pullfrog review at 0cee9a8.

  • Persisted lapse history Added nullable reason and ending timestamp state to User, EF persistence, and the subscription status response.
  • Mapped provider lifecycles Added Stripe payment failure and cancellation mapping plus Play account hold, cancellation, and expiration mapping.
  • Expanded lifecycle coverage Added domain, status query, Stripe webhook, and Play RTDN tests for the new state.
  • Refreshed architecture artifacts Regenerated the architecture map after the application changes.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread src/Orbit.Application/Subscriptions/Commands/HandlePlayNotificationCommand.cs Outdated
Comment thread src/Orbit.Application/Subscriptions/Commands/HandleWebhookCommand.cs Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

No new high-severity issues were introduced, but the two open billing-state integrity findings remain unresolved, so this revision is still not mergeable.

Reviewed changes Reviewed the mapping coverage and generated-artifact changes added since the prior Pullfrog review.

  • Expanded Play mapping coverage Added exact tests for account hold, cancellation preservation, expiration preservation, and revocation.
  • Expanded Stripe mapping coverage Added cancellation-details fixtures and assertions for cancellation requested, payment failed, and payment disputed.
  • Covered contract serialization Added direct tests for known and unknown lapse-reason API values.
  • Refreshed generated artifacts Updated the architecture map after adding the mapping test class.
  • Merged current base Incorporated the latest main changes without altering subscription behavior.

Pullfrog  | Fix it ➔View workflow run | Using GPT Sol𝕏

@sonarqubecloud

Copy link
Copy Markdown

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes Reviewed the provider ownership and Stripe lifecycle ordering changes added since the prior Pullfrog review.

  • Enforced provider ownership: Scoped pending lapse writes to the provider that owns the current entitlement and added coverage for both cross-provider directions.
  • Ordered Stripe outcomes: Persisted the latest accepted Stripe event time and applied it across checkout, renewal, failure, update, and deletion paths so older outcomes cannot overwrite newer state.
  • Covered stale delivery: Added a signed-webhook regression test that delivers invoice.paid before an older invoice.payment_failed and verifies the renewed state remains clear.
  • Persisted the ordering boundary: Added the nullable user column, EF migration, generated model metadata, and snapshot update for the Stripe event watermark.

Pullfrog  | View workflow run | Using GPT Sol𝕏

@thomasluizon
thomasluizon merged commit eec0782 into main Aug 23, 2026
26 checks passed
@thomasluizon
thomasluizon deleted the feature/ticket-344-lapse-reason branch August 23, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant