Repository navigation
Grant atomic referral Pro days (ORB-22) - #823
Conversation
There was a problem hiding this comment.
Caution
A delayed expiration or cancellation event can revoke already-granted referral Pro days. Preserve the independent bonus entitlement before merging.
Reviewed changes Reviewed referral qualification, atomic entitlement grants, delivery retries, provider interactions, migrations, and client compatibility across both commits.
- Qualification and caps: Three distinct local completion dates qualify each participant independently, with ordered account locks and a shared five-reward cap.
- Atomic Pro grants: Eligible accounts receive seven Pro days together with the referral grant timestamp; active paid subscribers retain coupon rewards.
- Background delivery: Hangfire delivers coupons and notifications with stable coupon-creation keys and per-recipient checkpoints.
- Schema and contracts: Additive migration/backfill, referral DTO/OpenAPI additions, and MCP wording; checked the consuming shared schema in
orbit-ui-mobile. - Verification: All 68 focused application, domain, and infrastructure tests passed. A standalone probe against the real domain code reproduced the entitlement loss for both Stripe and Google Play after ending the account's trial.
gpt-6.1-sol | 𝕏
There was a problem hiding this comment.
No new issues found. The resolved entitlement-loss finding is fixed, and no new High or Critical issues remain.
Reviewed changes Reviewed 0cc4f0b4 since the prior Pullfrog review, with the full PR diff as context.
- Separated bonus access: Preserved independently earned Pro days through provider cancellation, expiry, renewal, and new purchases.
- Updated entitlement consumers: Checked persisted-field mappings, calendar auto-sync, Play ownership comparisons, and deletion restoration against the split entitlement model.
- Verified migration and regressions: Reviewed repeatable normalization and rollback SQL; all 236 focused domain, application, and infrastructure tests passed locally, including terminal provider callbacks and exact bonus-expiry boundaries.
gpt-6.1-sol | 𝕏
There was a problem hiding this comment.
No new issues found.
Reviewed changes Reviewed the merge reconciliation in c86e2b9b since the prior Pullfrog review at 0cc4f0b4, using the full PR diff for context.
- Reconciled migration expectations: Updated the model-difference count and migration inventory to include
main's interleavedMakeSubHabitsFreemigration while preserving both referral migrations. - Verified persistence regressions: Ran all 16 focused migration and referral persistence tests successfully; no new PR-specific runtime behavior or contract changes were introduced. The previously resolved entitlement-loss concern remains fixed.
gpt-6.1-sol | 𝕏
|
…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)
* 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)




Closes thomasluizon/orbit-tickets#28. Implements ORB-22.
Referral completion previously saved
Rewardedbefore attempting Stripe delivery. A failed delivery could leave both users without their promised reward. Qualifying referrals now grant seven Pro days to each eligible account without an active paid subscription, in the same transaction asRewardGrantedAtUtc. Active subscribers retain the 10 percent coupon and their exact subscription expiry.User,Referral,ReferralRewardKindandAppConstantshold the entitlement guard, per-party reward choices, delivery checkpoints, seven-day grant and five-reward cap. Incoming and outgoing rewards share that cap, bounding one account to 35 bonus Pro days. Attribution remains capped separately at ten.CheckReferralCompletionCommandcounts three distinct local log dates inside the existing completion window, excluding logs before attribution, deleted logs, non-completions and future logs. Transaction-scoped account advisory locks serialize cap checks; the timestamp prevents duplicate grants. Failure recovery reloads only the changed referral and grant recipients, preserving the caller's tracked and pending work in a joined transaction.DeliverReferralRewardJobruns coupon creation/application and push delivery through Hangfire. Jobs are enqueued before the entitlement transaction commits, so queue failure leaves the grant retryable. A worker that sees the referral still pending retries after commit. Coupon creation passesreferral-reward-{referralId}-{userId}through the existingStripeCouponRewardServiceidempotency-key support. A current Play subscription stores its coupon for Play redemption even when a historical Stripe ID remains on the account.ProDaysPerReferraland reportpro_daysfor accounts without an active paid subscription. MCP describes the selected reward. Controller response annotations generate both OpenAPI contracts.Deploy the API before thomasluizon/orbit-tickets#62. Existing clients continue parsing the unconstrained
rewardTypestring and retain stale discount wording until that client ticket ships;DiscountPercentremains present. Pausing Hangfire delays coupon/push delivery while transactional Pro-day grants continue. Reverting this code does not claw back days already promised. Push can repeat if a worker crashes after sending and before saving its delivery checkpoint.Assumptions
ProcessReferralCodeCommandstill writes coupon pointers and the subscriber fallback still uses them. Attribution changes are explicitly out of scope.Referral; rejected a separate ledger table. Reuse the installed coupon service's existing key parameter rather than adding another Stripe integration.Kept existing SQL column names and mapped them to provider facts, rather than adding duplicate entitlement storage. No external response fields or DTO contracts changed.
None.
Test evidence
dotnet test tests/Orbit.Application.Tests/Orbit.Application.Tests.csproj --filter FullyQualifiedName~CheckReferralCompletionCommandHandlerTests --verbosity minimalpassed all 12 original tests.dotnet test tests/Orbit.Application.Tests/Orbit.Application.Tests.csproj --no-restore --filter FullyQualifiedName~Handle_ThreeLogsOnSameDay_DoesNotQualify --verbosity minimalfailed as intended: expectedPending, observedRewardedafter three same-day logs.dotnet test tests/Orbit.Application.Tests/Orbit.Application.Tests.csproj --no-restore --filter 'FullyQualifiedName~CheckReferralCompletionCommandHandlerTests|FullyQualifiedName~GetReferralStatsQueryHandlerTests|FullyQualifiedName~GetReferralDashboardQueryHandlerTests' --verbosity minimalpassed 33 tests, including the regression and retained-provider case. Repository queries execute against EF InMemory instead of returning an invented aggregate count.DbUpdateConcurrencyExceptioninto the loser after the winner commits. Stripe failure leaves the already committed Pro entitlement intact and retries with the same key.dotnet test tests/Orbit.Infrastructure.Tests/Orbit.Infrastructure.Tests.csproj --no-restore --filter FullyQualifiedName~Handle_QueueFailureInAmbientTransaction_PreservesCallerTracking --verbosity minimalfailed before the cleanup correction: caller language was lost. Strengthening it to include pending caller work also failed with zero persisted notifications instead of one. After scoped recovery, the infrastructure filter forReferralRewardTransactionTests|FeatureFlagAccountsMigrationTests|ReferralRewardMigrationTestspassed all seven tests.dotnet test tests/Orbit.Application.Tests/Orbit.Application.Tests.csproj --no-restore --filter FullyQualifiedName~ExecuteAsync_PlaySubscriberWithRetainedStripeId_StoresCouponForPlay --verbosity minimalfailed before the provider guard: the Play coupon pointer was null because delivery targeted the old Stripe subscription. The final focused suite passed all 33 tests, including the corrected Play path.dotnet build Orbit.slnxcompleted with zero errors. Each project passed withdotnet test tests/<project>/<project>.csproj --no-build --no-restore: Domain 780, Application 5,236, Infrastructure 3,776.dotnet test --no-build --no-restorepassed the full solution, including 40 analyzer tests, for 9,832 passing tests and zero failures.ORBIT_MIGRATION_GUARD_BASE=origin/main dotnet test tests/Orbit.Infrastructure.Tests/Orbit.Infrastructure.Tests.csproj --no-build --no-restore --filter FullyQualifiedName~RollingMigrationGateTestspassed all 59 migration guard tests. Both commits passed the normal formatting, root allowlist, dash, timelessness and suppression hooks.Unchanged tests passed with the defect present: 9 Domain tests and 80 handler tests.
New regressions failed before the implementation changed: all 6 Domain cases and all 8 handler cases, both commands exiting 1.
All regressions now pass. Final verification on the amended commit:
dotnet build Orbit.slnxdotnet test tests/Orbit.Domain.Testsdotnet test tests/Orbit.Application.TestsORBIT_MIGRATION_GUARD_BASE=origin/main dotnet test tests/Orbit.Infrastructure.TestsThe initial Infrastructure run exposed an outdated migration inventory assertion. Updated it to include the new migration, then reran successfully. Normal commit hooks passed.
All required verification passed by exit code:
dotnet build Orbit.slnxdotnet test tests/Orbit.Domain.Testsdotnet test tests/Orbit.Application.TestsORBIT_MIGRATION_GUARD_BASE=origin/main dotnet test tests/Orbit.Infrastructure.TestsFrom
src/Orbit.Api,dotnet ef migrations has-pending-model-changes --project ../Orbit.Infrastructure --startup-project . --no-buildexited 0 and reported no pending changes.Before resolution, this unchanged test exited 1, expecting 17 changes and finding 18:
After resolution, this command exited 0 with all 16 tests passing:
No separate strengthened-test red run was needed: the unchanged test already detected the merge mismatch. All commit hooks passed.
External interface evidence
The installed assemblies were inspected through runtime reflection, rather than guessing response members. Reproduce with a temporary
net10.0console project referencing the installedHangfire.Core1.8.25 andStripe.net52.4.2 assemblies, and printtypeof(IBackgroundJobClient).GetMethod("Create"),typeof(Job).GetProperty("Type"),typeof(Job).GetProperty("Args"),typeof(RequestOptions).GetProperty("IdempotencyKey"), theCouponService.CreateAsyncmethods,typeof(Coupon).GetProperty("Id"), and the threeAutomaticRetryAttributeproperties below. The observed schema was:src/Orbit.Infrastructure/Services/StripeCouponRewardService.cs:17,40already accepts the optional key and forwards it intoRequestOptions.IdempotencyKey; this PR supplies the stable referral/recipient value at the activation delivery callsite. Queue return IDs are not used as a success/failure discriminator. No additional provider response fields are read.The Hangfire enqueue documentation confirms that enqueue persists the job before returning; the target method runs through a worker. The implementation uses that ordering and retries when a worker observes an uncommitted referral.
Manual steps
None for the owner. Startup applies the migrations; no new settings or backfill. A rollback of the production API to a binary older than this change is the run's step and applies the included
Downfirst (dotnet ef database update 20261020023828_AddReferralProDayRewardsagainst the production connection), because a code-only rollback reads bonus accounts as Free; the run then checks that every bonus row has rawPlan = ProandPlanExpiresAtcovering its bonus expiry.None introduced by this merge. The commit remains local for the orchestrator to push.