Skip to content

refactor(api): consolidate Stripe price resolution + startup validation (#78) - #177

Merged
thomasluizon merged 2 commits into
mainfrom
issue-78
Jun 5, 2026
Merged

refactor(api): consolidate Stripe price resolution + startup validation (#78)#177
thomasluizon merged 2 commits into
mainfrom
issue-78

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

What

Hardens and consolidates the BR→BRL / non-BR→USD Stripe pricing path (the behavior already shipped; this is the best-quality follow-up).

  1. DRY — single price-resolution seam. The duplicated 2×2 (interval, isBrazil) price-ID switches in CreateCheckoutCommandHandler and GetPlansQueryHandler are consolidated behind one deep IPriceResolver (Resolve(interval, isBrazil)). The mapping now lives in exactly one place; both handlers consume the seam.
  2. Fail-fast startup validation. StripeSettings.ValidatePriceIds() (mirrors the existing JwtSettings.Validate() idiom) is wired into ValidateOrbitSecuritySettings, so the API refuses to boot when any of the four BRL/USD monthly/yearly price IDs is blank — naming the missing key(s) — instead of silently degrading users to the wrong currency.
  3. Tests. Unit tests for the resolver (all four combinations + invalid-interval) and the validation (throws on blank, passes when all set); BR + non-BR integration tests exercising /api/subscriptions/checkout (asserts the resolved price ID) and /api/subscriptions/plans (asserts currency) end-to-end with a faked IBillingService (no live Stripe).

Config decision

Config stays flat (MonthlyPriceIdBrl / YearlyPriceIdBrl / MonthlyPriceIdUsd / YearlyPriceIdUsd); the issue's nested Stripe:Prices:BRL:Annual suggestion was intentionally not taken. The audience collapses to one isBrazil bool, so the price space is a fixed 2×2 that PriceResolver fully hides from both callers — nesting would churn StripeSettings, the Render env-var names, and existing tests for zero caller-side gain. A WHY comment records this in PriceResolver.cs.

Validation

  • dotnet build Orbit.slnx — green.
  • Unit tests green: Application 1689, Domain 344, Infrastructure 915 (incl. the updated CreateCheckoutCommandHandlerTests / GetPlansQueryHandlerTests and the new PriceResolverTests / StripeSettingsTests).
  • Integration: the 7 new SubscriptionPricingIntegrationTests pass against a real Postgres orbit_test. The integration WebApplicationFactory now injects test config (JWT/Encryption/DB/Stripe price IDs) via environment variables — the sanctioned pattern per tests/CLAUDE.md — which also makes the suite hermetic and runnable in a git worktree (where the gitignored appsettings.Development.json is absent).
  • Pre-existing, unrelated failures observed (NOT caused by this PR): ApiKeyTests (free-user PayGate on list/revoke — also fails on main baseline) and UserFactsControllerTests.*AfterChat (require a live AI backend).

Deploy note

This turns a previously-silent misconfiguration into a hard boot failure (the intended AC). Render env must have all four Stripe:*PriceId* set before deploy — the error message names any missing key.

Merge note

This edits ServiceCollectionExtensions.cs in the Stripe DI block (~308-326) and the ValidateOrbitSecuritySettings method (~35). Issue #79 (in flight) edits a different region of the same file (AI-tool registration ~153-198), so a trivial rebase may be needed at merge — no logical conflict.

Refs thomasluizon/orbit-ui-mobile#78

@gitguardian

gitguardian Bot commented Jun 4, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
26951803 Triggered Generic Password aeab93e tests/Orbit.IntegrationTests/IntegrationTestWebApplicationFactory.cs View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@claude claude 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.

Clean consolidation of the 2×2 price-switch behind a single IPriceResolver seam, with fail-fast startup validation that matches the existing JwtSettings.Validate() idiom. The new PriceResolver correctly maps all four combinations and throws (not silently falls through) on an unknown interval. CapturingBillingService is a well-scoped test double — the AddScoped(_ => BillingService) pattern is correct for sequential tests reading the captured ID back immediately. The GitGuardian alert on the encryption key is a false positive: it's a dedicated test placeholder guarded by SetIfMissing, so any real env var wins. No contract changes, no authorization gaps, test coverage for every new code path including the startup-validation throw. Ship it.

@thomasluizon
thomasluizon changed the base branch from main to chore/fix-integration-jwt-secret June 4, 2026 19:24
@thomasluizon

Copy link
Copy Markdown
Owner Author

Rebased onto chore/fix-integration-jwt-secret (#176) and retargeted this PR's base to it, so it is now stacked on #176 to avoid a merge conflict in tests/Orbit.IntegrationTests/IntegrationTestWebApplicationFactory.cs.

Both PRs independently rewrote that factory:

Reconciliation in the factory:

No feature changes in this PR were touched — only the test factory file. Validation: dotnet build clean (0 errors); SubscriptionPricing integration tests pass 7/7 on the new base.

Merge #176 first, then this.

@claude claude 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.

Post-rebase re-review (rebased onto #176 to avoid factory conflict). The only delta from my prior approval is removing the now-redundant Jwt__SecretKey env-var from the static constructor — JWT secret injection is correctly delegated to #176's UseSetting path. All prior findings still hold: IPriceResolver DI seam is clean, StripeSettings.ValidatePriceIds() follows the existing JwtSettings.Validate() idiom, CapturingBillingService is correctly wired via AddScoped and safe under Sequential execution, and every new code path has unit and integration test coverage. Ship it after #176 merges.

@claude claude 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.

Post-rebase re-check on commit 1cb8212 (the Development exemption). The IsDevelopment() guard is the correct shape for a fail-fast startup check — non-dev environments (staging, prod) get the hard boot failure, local dev is exempt exactly as the PR description explains. No new correctness issues, no auth gaps, no contract changes. The rest of the review stands from my prior two approvals: IPriceResolver seam is clean, StripeSettings.ValidatePriceIds() follows the JwtSettings.Validate() idiom, PriceResolver correctly throws on unknown intervals rather than silently returning a wrong price ID, and the Sequential-collection CapturingBillingService pattern is safe. Ship after #176 merges.

Base automatically changed from chore/fix-integration-jwt-secret to main June 4, 2026 23:54
thomasluizon and others added 2 commits June 4, 2026 20:59
…on (#78)

Consolidate the duplicated 2x2 (interval x isBrazil) price-ID switches from
CreateCheckoutCommandHandler and GetPlansQueryHandler behind a single IPriceResolver
seam, so the mapping lives in exactly one place. Add fail-fast startup validation
(StripeSettings.ValidatePriceIds) wired into ValidateOrbitSecuritySettings so the API
refuses to boot when any of the four BRL/USD monthly/yearly price IDs is blank. Add unit
tests for the resolver and the validation, plus BR and non-BR integration tests covering
checkout price resolution and plans currency end-to-end with a faked IBillingService.

Refs thomasluizon/orbit-ui-mobile#78

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomasluizon
thomasluizon merged commit 017e5a6 into main Jun 5, 2026
2 of 3 checks passed
@thomasluizon
thomasluizon deleted the issue-78 branch June 5, 2026 00:01

@claude claude 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.

Clean consolidation of the Stripe 2×2 price-switch behind IPriceResolver, startup validation following the JwtSettings.Validate() idiom, and a IsDevelopment() guard that exempts local dev without silently degrading deployed environments. PriceResolver correctly throws on unknown intervals instead of the prior silent fallback to a wrong price ID. CapturingBillingService is correctly wired via AddScoped and safe under Sequential collection execution. The GitGuardian flag is a false positive — the encryption key is a test placeholder behind SetIfMissing, so any real env var wins. All new code paths have unit and integration test coverage. Ship it after #176 merges.

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