Skip to content

fix: expose discounted Stripe billed amount per period - #819

Merged
thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-385-billed-amount
Oct 20, 2026
Merged

thomasluizon merged 2 commits into
mainfrom
thomasluizon/ticket-385-billed-amount

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Closes thomasluizon/orbit-tickets#385

Couponed subscribers need the Stripe charge amount alongside the existing plan price. Billing details now append optional, nullable billedAmountPerPeriod, in minor units of the subscription currency. amountPerPeriod retains its existing meaning.

StripeBillingService previews the next invoice for the requested subscription and maps Stripe's AmountDue. Stripe owns discounts, duration, rounding, taxes and credits. invoice_upcoming_none yields a null billed amount while preserving the subscription. Other preview failures retain the billing-unavailable response, transient failures retry, and caller cancellation propagates.

The value passes through BillingSubscriptionDetails, GetBillingDetailsQueryHandler, and BillingDetailsResponse. The billing controller now declares its response DTO because its generated success response previously had no schema. Both OpenAPI documents include the optional field. The architecture map was regenerated locally. Behavior tests exercise the real service and query handler with SDK subscription, referral coupon and invoice objects; existing service and deletion tests mock the added invoice call.

The paired UI ticket thomasluizon/orbit-tickets#386 consumes this field after the API deploys.

Stripe interface evidence

The project and restored package use Stripe.net 52.4.2. Its installed ~/.nuget/packages/stripe.net/52.4.2/stripe.net.nuspec identifies source commit f4f1c8ce704825d7d7f00b4c9d848a2d028757eb. I downloaded that exact source revision and read the declarations before using the members:

  • InvoiceService.CreatePreviewAsync returns Task<Invoice> from /v1/invoices/create_preview and accepts options, request options and a cancellation token.
  • InvoiceCreatePreviewOptions confirms Subscription, PreviewMode, Discounts and SubscriptionDetails. Only Subscription is supplied, so Stripe inherits applicable discounts.
  • Invoice.AmountDue and Currency confirm a long final charge amount and the currency field. These shapes also appear in the installed lib/net9.0/Stripe.net.xml documentation.
  • StripeError.Code and StripeException confirm the error members and constructor. Stripe's error code reference defines the no-upcoming-invoice value.
  • Test fixtures follow that revision's Subscription, SubscriptionItem, Price, PriceRecurring, StripeList<T>, Discount, DiscountSource and Coupon entity sources, including Discount.Source.Coupon, PercentOff and Duration.

The installed 52.3.0 package named in the ticket identifies commit f4cd0d3e3e8fdab648b61711bafdcd6e897a3ec7; I also checked its InvoiceService.CreatePreviewAsync, InvoiceCreatePreviewOptions.Subscription and Invoice.AmountDue source declarations. The dependency remains at the version already selected by the repository.

To reproduce the source inspection, read the installed package's nuspec repository commit, download https://codeload.github.com/stripe/stripe-dotnet/tar.gz/<commit>, and inspect the paths linked above. No live customer records or Stripe credentials are needed.

Assumptions

  • Use the repository's installed Stripe.net 52.4.2; reject downgrading to the ticket's older 52.3.0 reference.
  • Use the next invoice's final amount due; reject the long-term recurring preview, which excludes one-time coupons and does not support cancellations, trials or pauses. The preview reference documents these differences.
  • An existing zero-due invoice maps to zero; reject treating zero as absence. Null represents Stripe reporting no upcoming invoice.
  • Extend the existing billing query validation and domain model unchanged because this read-only response addition introduces no new request input or entity mutation; reject adding unrelated validators or entity guards.
  • Used the installed 52.4.2 dependency; rejected downgrading to the ticket’s older version.
  • Preserved public BilledAmountPerPeriod; rejected renaming the append-only contract.
  • Used recurring line subtotals minus Stripe-calculated discounts; rejected invoice AmountDue, which includes unrelated billing adjustments.

Test evidence

  • Before implementation, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~StripeBillingServiceTests passed all 43 unchanged tests with the defect present.
  • Before implementation, dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~GetBillingDetailsQueryHandlerMappingTests passed all 4 unchanged mapping tests with the defect present.
  • With the defect still present, dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~StripeBillingDetailsTests compiled and failed all 11 strengthened behavior cases. Handle_MapsStripeInvoiceAmountWithoutChangingPlanPrice and the no-invoice cases failed because BilledAmountPerPeriod was absent; failure, retry and cancellation cases failed because the preview was never called.
  • After implementation, dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter 'FullyQualifiedName~StripeBillingDetailsTests|FullyQualifiedName~StripeBillingServiceTests|FullyQualifiedName~DeletionCancellationPersistenceTests' passed all 73 cases, including all 11 new cases. The unchanged application mapping tests also passed all 4 cases.
  • dotnet build Orbit.slnx: zero errors.
  • dotnet test tests/Orbit.Domain.Tests --no-build: 767 passed, exit 0.
  • dotnet test tests/Orbit.Application.Tests --no-build: 5,224 passed, exit 0.
  • dotnet test tests/Orbit.Infrastructure.Tests --no-build: 3,782 passed, exit 0.
  • dotnet test tests/Orbit.Analyzers.Tests --no-build: 40 passed, exit 0.
  • All four suites completed with zero failures and zero skipped tests, totaling 9,813 passing tests.
  • Both OpenAPI documents passed oasdiff breaking origin/main:src/Orbit.Api/<document> src/Orbit.Api/<document> --fail-on ERR with no breaking changes.
  • node tools/arch-map.mjs completed. Generated architecture files remain local.
  • Before changes: dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~StripeBillingDetailsTests --no-restore passed all 11 existing tests with the defects present.
  • Before the fix: dotnet test tests/Orbit.Infrastructure.Tests --no-restore --filter FullyQualifiedName~StripeBillingDetailsTests failed 15 tests, including the strengthened availability and recurring-amount regressions.
  • GetSubscriptionDetails_DoesNotRequestInvoicePreview separately failed before the fix because it observed one preview call.
  • The Play regression failed before the fix under dotnet test tests/Orbit.Application.Tests --no-restore --filter FullyQualifiedName~GetBillingDetailsQueryHandlerTests.
  • After the fix, focused runs passed: dotnet test tests/Orbit.Infrastructure.Tests --no-build --filter FullyQualifiedName~StripeBillingDetailsTests (23 tests) and dotnet test tests/Orbit.Application.Tests --no-build --filter FullyQualifiedName~GetBillingDetailsQueryHandler (9 tests).
  • dotnet build Orbit.slnx: exit 0, zero errors.
  • Broader suites each exited 0: dotnet test tests/Orbit.Domain.Tests (767), dotnet test tests/Orbit.Application.Tests (5,226), and dotnet test tests/Orbit.Infrastructure.Tests (3,794).
  • oasdiff breaking origin/main:src/Orbit.Api/openapi.json src/Orbit.Api/openapi.json --fail-on ERR: exit 0, no breaking changes.
  • Regenerated architecture files without staging them. All commit hooks passed.

Stripe evidence: the installed dependency is 52.4.2, confirmed through its nuspec and resolved assets. Inspected its pinned source and cross-checked the relevant entity/service members against 52.3.0. The implementation reads preview options, invoice currency/lines, pagination, recurring parent details, line subtotal, and Stripe-calculated discount amounts. Installed SDK source, 52.3.0 source.

Checked Stripe’s current references on 2026-10-20. Recurring previews exclude one-time billing effects and do not support trials, cancellations, or pauses. Those cases return null. Preview reference.

Confirmed error.code parsing in the installed SDK and invoice_upcoming_none in Stripe’s reference. Its HTTP status remains unconfirmed; the fixture leaves it unset. SDK error parser, Error reference.

Manual steps

The production API release of the go-live runbook carries this change; the run verifies billedAmountPerPeriod on GET /api/subscriptions/billing after it, before the paired UI ticket thomasluizon/orbit-tickets#386 ships.

No new environment variables, secrets, dashboard settings, migrations, or backfills are required.

@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

The new invoice-preview dependency can prevent account-deletion jobs from stopping Stripe renewal, leaving subscriptions able to charge after deletion is requested.

Reviewed changes: Reviewed the billed-amount addition, Stripe preview semantics, billing response mapping, OpenAPI updates, lifecycle callers, and existing client compatibility.

  • Stripe amount lookup: Previews the requested subscription's next invoice and maps AmountDue, preserving zero and treating invoice_upcoming_none as absence.
  • Response contract: Appends nullable billedAmountPerPeriod without changing the existing plan price or other shipped fields.
  • API documentation: Declares the billing response DTO and adds its schemas to both OpenAPI documents.
  • Behavior tests: Covers mapping, missing previews, provider failures, retries, and cancellation; all 73 focused tests passed. A separate temporary unit reproducer confirmed the cancellation regression described inline and was removed afterward.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread src/Orbit.Infrastructure/Services/StripeBillingService.cs Outdated
@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 d5dd870c since the prior Pullfrog review, checked the resolved discussion against the current code, and found no remaining blocking issues.

  • Isolated invoice previews: Removed the preview dependency from subscription-state reads used by deletion cancellation, restoration and owed-cancellation jobs.
  • Refined recurring amounts: Calculated the recurring amount from subscription-line subtotals minus Stripe-calculated discounts, excluding prorations, one-time charges and customer balance.
  • Preserved billing availability: Returned a nullable amount for unavailable or unusable previews while retaining retries and caller cancellation propagation; skipped preview calls for unsupported subscription states.
  • Protected provider boundaries: Rejected Play subscribers with historical Stripe identifiers and preserved the append-only response contract for existing clients.
  • Verified regressions: Passed all 85 focused infrastructure tests and 34 focused application tests, with zero failures or skipped tests.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@thomasluizon
thomasluizon merged commit 0460a54 into main Oct 20, 2026
23 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-385-billed-amount branch October 20, 2026 04:33
thomasluizon added a commit that referenced this pull request Oct 20, 2026
…e sub-habits and referral Pro days (#746) (#829)

* fix: preserve yearly streak gap repairs through recalculation (#818)

* fix: preserve yearly streak gap repairs through recalculation

* fix: isolate yearly streak repair history window

(cherry picked from commit 90c5320)

* fix: expose discounted Stripe billed amount per period (#819)

* fix: expose discounted Stripe billed amount per period

* fix: isolate optional recurring Stripe billing preview

(cherry picked from commit 0460a54)

* Make sub-habits free across every creation path (ORB-231) (#824)

* feat: make sub-habits available to every plan (ORB-231)

* test: track free sub-habits in migration model expectations

* fix: preserve Pro slip alerts when creating free sub-habits

(cherry picked from commit c9d22a6)

* Grant atomic referral Pro days (ORB-22) (#823)

* feat: grant atomic referral Pro days (ORB-22)

* fix: preserve referral transaction and billing context

* fix: preserve bonus Pro access across provider changes

(cherry picked from commit 7b4e9af)

* test: retain combined migration assertions after main carry

* fix: include one-time coupons in billed amount preview (#830)

(cherry picked from commit 377eca5)
thomasluizon added a commit that referenced this pull request Oct 20, 2026
* fix: expose discounted Stripe billed amount per period

* fix: isolate optional recurring Stripe billing preview

(cherry picked from commit 0460a54)
thomasluizon added a commit that referenced this pull request Oct 20, 2026
Carry the redesign-adapted fixes from a80cf56 for #818, #819, #823, #824 and #830 without replaying their main implementations. Preserve ordinary merge ancestry and the production keep-list for thomasluizon/orbit-tickets#1579.
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