Skip to content

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

Merged
thomasluizon merged 3 commits into
mainfrom
thomasluizon/ticket-237-subhabits-free
Oct 20, 2026
Merged

thomasluizon merged 3 commits into
mainfrom
thomasluizon/ticket-237-subhabits-free

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Free users could not create sub-habits through the dedicated endpoint, inline creation, bulk creation, duplication, or Astra. Remove the server gate from all five paths so both plans can create sub-habits while retaining the existing depth validation and root-habit ceiling.

Closes thomasluizon/orbit-tickets#237. Implements ORB-231.

Intended approach

  • Delete CanCreateSubHabits from PayGateService and IPayGateService, remove AppConfigKeys.SubHabitsProOnly, and remove each call and failure branch in the four habit commands and CreateHabitTool. Delete the dedicated command's unused pay-gate dependency.
  • Change only sub_habits.PlanRequirement in OrbitDbContext seed data. Scaffold MakeSubHabitsFree through OrbitDbContextFactory, keep its Designer and snapshot, and add the non-model-seeded config deletion and matching rollback. Deleting enforcement prevents the old default from restricting environments without a config row.
  • Replace obsolete gate mocks and rejection tests with SubHabitPlanAccessTests, which uses the real PayGateService for all five paths and both plans. Check child ownership and parent links, copied descendants, saved output, depth-six rejection, the root ceiling, and the interface surface. Extend ConfigControllerTests over the real seeded model and feature resolver; test both migration directions in MakeSubHabitsFreeMigrationTests. Extend FeatureFlagAccountsMigrationTests to include the new migration and assert its exact seed update in the current model.
  • The build regenerated OpenAPI output without a tracked difference. No response field changes, new validators, domain rules, or external response-field reads are introduced.

Assumptions

  • Apply the work order's cap decision: only active root habits count toward the existing ceiling. Reject the original scenario 8 reading that would add a new cap check to dedicated child creation. The test creates a child at the root ceiling, proves the count stays fixed, and proves one additional root allowance still permits one root.
  • Use the existing xUnit unit-test projects and EF InMemory controller setup, rather than adding the original ticket's integration or live-database harness. The work order prohibits servers, app sessions, browsers, and live migration application, so local HTTP and Astra account checks were not performed.
  • Keep the supplied thomasluizon/ticket-237-subhabits-free branch, rather than creating a branch with the repository's usual prefix, as the work order explicitly requires.

Placed the shared check in Habits/Services rather than expanding IPayGateService; it evaluates constructed habits using the existing entitlement.

Test evidence

Before changing implementation, the five existing success tests passed with the restriction present:

dotnet test tests/Orbit.Application.Tests --no-build --filter 'FullyQualifiedName~CreateSubHabitCommandHandlerTests.Handle_ValidCommand_CreatesChild|FullyQualifiedName~CreateHabitCommandHandlerTests.Handle_WithSubHabits_CreatesParentAndChildren|FullyQualifiedName~BulkCreateHabitsCommandHandlerTests.Handle_WithSubHabits_CreatesParentAndChildren|FullyQualifiedName~DuplicateHabitCommandHandlerTests.Handle_WithChildren_DuplicatesChildHabits|FullyQualifiedName~CreateHabitToolTests.WithSubHabits_CreatesParentAndChildren'

Observed: 5 passed, 0 failed, 0 skipped. They substituted a successful sub-habit gate and therefore did not exercise the free-plan restriction.

Before removing the gate:

dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~SubHabitPlanAccessTests

Observed: 8 failed, 6 passed, 0 skipped. All five free-plan creation cases failed with This is a Pro feature.; the free depth and child-at-ceiling cases were blocked by the same gate, and the interface assertion found the old member. All five Pro creation cases and Pro depth rejection passed. These new tests exercise the real service with its compiled defaults.

Before changing the feature seed:

dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~GetConfig_SubHabitsAreFree_PreservesLimits

Observed: 2 failed, 0 passed, 0 skipped. The free account received enabled: false; the Pro account received a string plan requirement rather than null.

After gate deletion and seed change, before adding the hand-written migration operations:

dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~ConfigControllerTests|FullyQualifiedName~MakeSubHabitsFreeMigrationTests'

Observed: 9 passed, 2 failed, 0 skipped. Both migration tests found only one operation, catching the missing config deletion and rollback insertion. After completing the migration, the same filter with --no-build passed all 11 tests. The application handler/tool/service filter passed all 165 focused tests after gate deletion.

The first full Infrastructure run (dotnet test tests/Orbit.Infrastructure.Tests) found one required model-expectation update: FeatureFlagAccountsMigrationTests.TargetModel_PreservesRedesign expected 12 operations and eight recent migrations. The new feature seed adds a thirteenth operation and a ninth migration. Updated the test to assert exactly one UpdateDataOperation for AppFeatureFlags.sub_habits.PlanRequirement = null, and list the new migration. No production changes were needed. The focused command below then passed 14 tests with no failures or skips:

dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~FeatureFlagAccountsMigrationTests|FullyQualifiedName~MakeSubHabitsFreeMigrationTests|FullyQualifiedName~ConfigControllerTests'

Final verification, each command exited 0:

Command Result
dotnet build Orbit.slnx 0 errors; existing package/versioning/obsolete-API warnings remain
dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~SubHabitPlanAccessTests 14 passed, 0 failed, 0 skipped
dotnet test tests/Orbit.Domain.Tests 767 passed, 0 failed, 0 skipped
dotnet test tests/Orbit.Application.Tests 5,227 passed, 0 failed, 0 skipped
dotnet test tests/Orbit.Infrastructure.Tests 3,775 passed, 0 failed, 0 skipped

The final build and full Infrastructure run were repeated after the model-expectation-only follow-up commit. Domain and Application results are from the same production code. Both commits passed the normal formatting, dash, timeless prose, root-allowlist, and suppression hooks. Source searches leave CanCreateSubHabits/SubHabitsProOnly only in migration history; the removed paywall message is absent from src/. No new warning suppressions or narration comments were added.

Before implementation, ran:

dotnet test tests/Orbit.Application.Tests --filter FullyQualifiedName~SubHabitPlanAccessTests --logger 'console;verbosity=minimal'
  • Unchanged tests on dfe0dcbe: 14 passed.
  • Expanded tests before the fix: 19 passed, 3 failed as intended. Free dedicated, astra-explicit, and astra-default requests incorrectly succeeded.
  • After the fix: 106 focused tests passed.

Broader verification:

Command Result
dotnet build Orbit.slnx Exit 0; zero errors, 18 warnings
dotnet test tests/Orbit.Domain.Tests Exit 0; 767 passed
dotnet test tests/Orbit.Application.Tests Exit 0; 5,235 passed
dotnet test tests/Orbit.Infrastructure.Tests Exit 0; 3,775 passed

No skipped tests. OpenAPI regenerated without changes. All commit hooks passed.

External interface evidence

The new migration tests obtain UpOperations and DownOperations from the real MakeSubHabitsFree migration through EF Core 10.0.13; there are no operation fixtures. The incomplete scaffold's actual failed assertion printed an UpdateDataOperation with Table = AppFeatureFlags, KeyColumns = {Key}, KeyValues = {{sub_habits}}, Columns = {PlanRequirement}, and Values = {{null}} for Up or {{Pro}} for Down. The completed migration tests exercise the actual deletion and insertion operations and their key/column/value arrays. Re-derive these observations with the migration test filter above. No production code begins reading an external response field.

Migration

20261020030817_MakeSubHabitsFree:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.UpdateData(
        table: "AppFeatureFlags",
        keyColumn: "Key",
        keyValue: "sub_habits",
        column: "PlanRequirement",
        value: null);

    migrationBuilder.DeleteData(
        table: "AppConfigs",
        keyColumn: "Key",
        keyValue: "SubHabitsProOnly");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.UpdateData(
        table: "AppFeatureFlags",
        keyColumn: "Key",
        keyValue: "sub_habits",
        column: "PlanRequirement",
        value: "Pro");

    migrationBuilder.InsertData(
        table: "AppConfigs",
        columns: new[] { "Key", "Description", "Value" },
        values: new object[] { "SubHabitsProOnly", "Whether sub-habit creation is restricted to Pro users", "true" });
}

Scaffold command, run from src/Orbit.Api:

dotnet ef migrations add MakeSubHabitsFree --project ../Orbit.Infrastructure --startup-project . --context OrbitDbContext --verbose

The tool reported Using DbContext factory 'OrbitDbContextFactory'. No live database was contacted or migrated. Removed the new Designer's generated warning pragma to meet the ticket's zero-new-suppression requirement.

Manual steps

  • After merge, deploy the API before the paired UI release. In GitHub Actions, run Release API (.github/workflows/release.yml) from main with environment=production and branch=main. Startup applies the migration; no manual SQL, environment key, or backfill is required.
  • After deployment, read authenticated GET /api/config and confirm features.sub_habits has enabled: true and planRequirement: null, with the existing limits retained apart from the deleted obsolete gate key. From a free account, create a child through the REST endpoint and Astra and confirm both requests succeed and persist it. These live checks belong to release verification and were not run by this worker.

None for the owner. The run carries this change to api redesign/main through the #746 carry; it reaches production in the go-live runbook's production API release. After that release the run verifies ordinary free sub-habit creation succeeds, free creation with alerts returns PAY_GATE without creating habits, and Pro creation saves alerts. No new configuration keys, secrets, manual 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

Free sub-habit creation also exposes Pro-only slip alerts through the dedicated endpoint. Preserve that independent entitlement check before merging.

Reviewed changes Reviewed all five creation paths, entitlement removal, feature configuration, migration artifacts, and regression coverage.

  • Creation access: Removes the sub-habit paywall from dedicated, inline, bulk, duplicate, and Astra creation while retaining existing root-ceiling and depth behavior.
  • Feature migration: Makes sub_habits plan-independent and removes the obsolete gate configuration, with rollback operations and matching model snapshots.
  • Regression coverage: Adds Free/Pro creation and configuration tests; 165 focused Application tests and 19 focused Infrastructure tests passed. A temporary unit test reproduced the slip-alert bypass and was removed afterward.
  • Consumer compatibility: Checked the sibling config schema, which already accepts nullable plan requirements and does not require the removed configuration key.

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

Comment thread src/Orbit.Application/Habits/Commands/CreateSubHabitCommand.cs

@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 6bd8f346 since the prior Pullfrog review at dfe0dcbe, with the complete PR diff as context; the resolved slip-alert finding is fixed, and no new Critical or High issues remain.

  • Preserved paid alerts: Added a shared constructed-habit check before persistence in dedicated child, root/inline, and Astra creation, enforcing CanUseSlipAlerts without restoring the sub-habit paywall.
  • Expanded regression coverage: Added Free/Pro cases for dedicated, inline, and explicit/default Astra alerts, asserting free failures persist nothing and Pro requests retain alerts. Confirmed ordinary creation remains available across all five paths, while bulk and duplicated habits keep alerts disabled.
  • Verified focused suites: Passed 173 Application tests and 19 Infrastructure tests with no failures or skips. The full test suite and live deployment checks were not rerun in this review.

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

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit c9d22a6 into main Oct 20, 2026
27 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-237-subhabits-free branch October 20, 2026 04:58
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
* 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)
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