Skip to content

fix(seed): never let the demo seed cleanup delete the Owner's data - #1083

Merged
mforce merged 2 commits into
mainfrom
fix/1081-demo-cleanup-owner-data
Oct 5, 2026
Merged

mforce merged 2 commits into
mainfrom
fix/1081-demo-cleanup-owner-data

Conversation

@mforce

@mforce mforce commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Closes #1081

What and why

On a farm with no flocks, a demo seed that fails late (for example because the Owner deactivated the Small grade) ran its cleanup. That cleanup deleted every customer, order and order line of the farm, including the Owner's own.

The seed now refuses up front, with Failed and an actionable message, when the farm already has a customer. This is option (a) from the issue. I chose it over (b), which tracks the ids this run inserted, because:

  • It is one read and one if. Option (b) needs id tracking through every Commerce write, and the purge predicates would have to change.
  • It matches the existing precondition that any flock stops the seed. The cleanup stays inside the seeder's transaction and retry strategy, unchanged.

Every purged table is covered:

Purged table What proves its rows are the demo's
Customers new customer check
SalesOrders CustomerId is required, with an FK to Customers (RESTRICT)
SalesOrderItems FK to SalesOrders
BirdMovements FK to Flocks; existing flock check
DailyEntries, EggLots FlockId is required. These have no DB FK to Flocks (858 already records this), but only handlers write them, every handler needs an existing flock, and nothing deletes a flock
DailyEntryGrades FK to DailyEntries
EggInventoryMovements FK to EggLots

The check runs after #1076's product check, so the product-mismatch tests keep their customer canary unchanged. It reads through the tenant filter, because the tenant is already resolved at that point. That follows CommerceFixture's convention, so it needs no tenant-bypass registry entry.

A retry after a failed run still works: the cleanup removes the demo's customers, so the next run passes the check (LateFailure_LeavesTheFarmReseedable stays green).

Change map

File Change
src/Cluckwork.Infrastructure/Persistence/DemoDataSeeder.cs Refuses a farm with customers before any write. The cleanup comment now names both guards.
src/Cluckwork.Application/Features/Sales/ICommerceFixture.cs, src/Cluckwork.Infrastructure/Repositories/CommerceFixture.cs New AnyCustomerAsync, a tenant-filtered read.
tests/Cluckwork.Api.IntegrationTests/DemoSeedCleanupTests.cs New OwnersSales_SurviveAFailedSeed, Astra's repro with no fault injection. OwnerScopeAsync is extracted so it can be shared, and CreateOwnerCatalogAsync also returns the customer id.
docs/decisions/858-platform-composition.md The guard row now records the precondition.
src/AGENTS.md The seed paragraph now states both demo preconditions.

Mutations

All runs use DemoSeedCleanupTests, under sg docker.

Mutation Expected Observed
None: new test on base product code (commit 879ab9c) new test red 6 passed / 1 failed. OwnersSales_SurviveAFailedSeed: Customers, SalesOrders and SalesOrderItems each Expected: 1, Actual: 0
M1: disable the customer check (if (false && …)) new test red; other-farm case green 6 passed / 1 failed (OwnersSales_SurviveAFailedSeed); Cleanup_LeavesAnotherFarmsDemoRows green
M2: AnyCustomerAsync ignores the tenant filter, so it counts customers on every farm other-farm case red 4 passed / 3 failed: Cleanup_LeavesAnotherFarmsDemoRows, LateFailure_RemovesEveryFlockRootedAndCommerceRow, FailedDelete_RollsBackEveryEarlierDelete. Only the first fails on every run; the other two depend on test order
Restored head all green 7 passed

Test counts (measured locally, one class at a time)

Class Base (origin/main 17e3786 product code) Head
DemoSeedCleanupTests 6 passed (+ new test red) 7 passed
DemoSeedTests – 2 passed
SeedCommandTests – 11 passed
DemoSeedNoOwnerTests / DemoSeedAttributionTests / DemoSeedDisabledOwnerTests / DemoSeedOnlyDisabledOwnerTests – 1 / 1 / 1 / 1 passed
AccessSeederActorTests / AccessSeedOwnerFailureTests – 1 / 4 passed
FixturePortRegistrationTests – 7 passed
SchemaDocsTests (image pins) – 4 passed
Application.Tests ~Architecture – 371 passed
Application.Tests ~TenancyDocsFreshnessTests|~TenantBypass – 65 passed

Base counts were measured only for the class this PR changes. I did not run the full suite; CI is the authority.

Not covered

  • A farm that has customers but no flocks can no longer be demo-seeded. That is intended: the operator gets a message that names provision-account.
  • DailyEntries and EggLots still have no DB FK to Flocks. The flock check covers them only as long as the application invariant above holds. This PR does not change that.

@mforce

mforce commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

Review by Codex (gpt-6-astra) at 61eb69e

Verdict: no P1/P2 or qualifying P3 findings. The refusal protects pre-existing Owner data reachable through ordinary operations.

I reproduced the mutations, retaining the new regression test:

Variant Result
Base seeder, reverting only DemoDataSeeder.cs 6 passed / 1 failed. Only OwnersSales_SurviveAFailedSeed failed: customer, order and line each expected 1, got 0. This also verifies removing the guard.
Head 7 passed
AnyCustomerAsync counts every farm through IgnoreQueryFilters 4 passed / 3 failed, including Cleanup_LeavesAnotherFarmsDemoRows. The other failures were LateFailure_RemovesEveryFlockRootedAndCommerceRow and FailedDelete_RollsBackEveryEarlierDelete.
Restored head 7 passed

I traced all four cleanup calls. The nine explicit deletes are unchanged:

Tables Protection for existing rows
Customers New customer precondition
SalesOrders, SalesOrderItems Required foreign keys lead to a customer; deleting that customer is restricted
Flocks, BirdMovements Existing flock precondition; movements have a flock FK
DailyEntries, EggLots Entry creation requires an existing flock; lot creation uses those entries; ordinary flock lifecycle archives rather than deletes
DailyEntryGrades, EggInventoryMovements Required foreign keys to entries and lots

There is also a tenth affected table, SalesOrderAllocations, deleted through order/line cascades. The customer precondition covers it. DailyEntries and EggLots indeed lack database FKs to flocks, so their protection depends on the application lifecycle above.

Payments are not purged and restrict deletion of both their customer and order. Customers have no archive/delete operation. Product prices and mappings, general inventory and expenses are outside the delete/cascade set. Discount details live on orders and lines.

Five temporary probes verified:

  • A customer-only farm returns Failed, names provision-account, and adds no products or audit rows.
  • An archived flock preserves its entries, grades, lots and movements and returns AlreadySeeded.
  • A missing flock cannot create a daily entry.
  • An ordinary missing-Small failure preserves the Owner's product details/pricing and inventory purchase/adjustment records.
  • PostgreSQL refuses deleting a customer under an order; draft-order payment is refused; the seed preserves the sales rows. My probe initially expected SQLSTATE 23503; correcting it to the actual 23001 (RESTRICT) made its remaining assertions pass.

The customer query uses the tenant filter. The precondition runs after tenant resolution and product compatibility, before writes and outside the cleanup catch. Its message is actionable; the two documentation edits match that behavior. The existing fixture contract/implementation and seeder reach declarations suffice; no new ledger, seam or bypass entry is needed.

AdapterReachRealTreeTests, ModuleLedgerRealTreeTests and SeamSurfaceRealAssemblyTests each passed 2/2. Tenant-bypass classes plus TenancyDocsFreshnessTests passed 65 tests. PeerContractRealTreeTests passed 1/1 and CompatibilityExceptionRealTreeTests passed 2/2. Restored-head integration checks also passed: FixturePortRegistrationTests 7/7, DemoSeedTests 2/2 and SeedCommandTests 11/11.

Both requested quality skills found no issue in touched lines. Ponytail review: Lean already. Ship.

Every test invocation used DOTNET_USE_POLLING_FILE_WATCHER=1 sg docker -c 'dotnet test <project> --filter FullyQualifiedName~<Class>', one class at a time. No full integration suite was run. Concurrent writers were not tested. All temporary mutations were restored with git checkout -- .; the detached worktree is clean.

@mforce
mforce merged commit d525a0e into main Oct 5, 2026
19 checks passed
@mforce
mforce deleted the fix/1081-demo-cleanup-owner-data branch October 5, 2026 07:48
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.

fix(seed): demo seed cleanup can delete the Owner's customers and orders on a farm with no flocks

1 participant