From 8a9786236cb4d6f9c690f490a35644fe4735ec96 Mon Sep 17 00:00:00 2001 From: Cramsan Date: Sat, 25 Jul 2026 23:59:43 -0700 Subject: [PATCH 1/5] [EDIFIKANA] Mark DS-OCC-008 fixed in datastore test catalog (NONE) --- edifikana/back-end/docs/datastore-test-catalog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/edifikana/back-end/docs/datastore-test-catalog.md b/edifikana/back-end/docs/datastore-test-catalog.md index dd3556a35..68fe92dc7 100644 --- a/edifikana/back-end/docs/datastore-test-catalog.md +++ b/edifikana/back-end/docs/datastore-test-catalog.md @@ -357,7 +357,10 @@ datastore query bodies, and mapper functions side by side — not assumptions fr | DS-OCC-006 | P1 | `updateOccupant(isPrimary = false)` on a currently-primary occupant | Confirms the boolean-null-skip distinction (§3) — must actually flip to `false`, not skip | | DS-OCC-007 | P1 | `softRemoveOccupant` sets both `status = INACTIVE` and `end_date = today` atomically in one update | Confirm both fields change together; occupant remains readable via `getOccupant` (only `deleted_at` gates that, not `status`) but is excluded from `listOccupantsForUnit(includeInactive = false)` | | DS-OCC-008 | P2 | `purgeOccupant` on an active (non-deleted, non-removed) occupant | Fixed — guard now refuses (returns `false`), matching the consistent group. Also added `deleteOccupant` (Occupant previously had no way to soft-delete at all) | +<<<<<<< HEAD | DS-OCC-009 ⚠️ | P2 | `purgeOccupant` on a **nonexistent** id | **NOT YET IMPLEMENTED** — no test exercises this case today. Expected: `Result.success(false)`, no error (zero rows matched by the guard's `select()`), same shape as every other consistent-group resource's nonexistent-id purge case. Surfaced as a coverage gap while verifying `DS-OCC-008`'s fix — mirrors `DS-EMP-010`/`DS-EVT-013`, the analogous gaps for `updateEmployee`/`updateEventLogEntry` | +======= +>>>>>>> 907dd089f ([EDIFIKANA] Mark DS-OCC-008 fixed in datastore test catalog (NONE)) ### 4.8 `SupabaseOrganizationDatastore` (tables: `organizations`, `user_organization_mapping`) From 1da4bf9689bf8647cf8845e7d272cb3bf1fc658d Mon Sep 17 00:00:00 2001 From: Cramsan Date: Sun, 26 Jul 2026 00:37:04 -0700 Subject: [PATCH 2/5] [EDIFIKANA] Mark DS-ORG-010 and DS-X-005 fixed in datastore test catalog (NONE) --- edifikana/back-end/docs/datastore-test-catalog.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/edifikana/back-end/docs/datastore-test-catalog.md b/edifikana/back-end/docs/datastore-test-catalog.md index 68fe92dc7..dd3556a35 100644 --- a/edifikana/back-end/docs/datastore-test-catalog.md +++ b/edifikana/back-end/docs/datastore-test-catalog.md @@ -357,10 +357,7 @@ datastore query bodies, and mapper functions side by side — not assumptions fr | DS-OCC-006 | P1 | `updateOccupant(isPrimary = false)` on a currently-primary occupant | Confirms the boolean-null-skip distinction (§3) — must actually flip to `false`, not skip | | DS-OCC-007 | P1 | `softRemoveOccupant` sets both `status = INACTIVE` and `end_date = today` atomically in one update | Confirm both fields change together; occupant remains readable via `getOccupant` (only `deleted_at` gates that, not `status`) but is excluded from `listOccupantsForUnit(includeInactive = false)` | | DS-OCC-008 | P2 | `purgeOccupant` on an active (non-deleted, non-removed) occupant | Fixed — guard now refuses (returns `false`), matching the consistent group. Also added `deleteOccupant` (Occupant previously had no way to soft-delete at all) | -<<<<<<< HEAD | DS-OCC-009 ⚠️ | P2 | `purgeOccupant` on a **nonexistent** id | **NOT YET IMPLEMENTED** — no test exercises this case today. Expected: `Result.success(false)`, no error (zero rows matched by the guard's `select()`), same shape as every other consistent-group resource's nonexistent-id purge case. Surfaced as a coverage gap while verifying `DS-OCC-008`'s fix — mirrors `DS-EMP-010`/`DS-EVT-013`, the analogous gaps for `updateEmployee`/`updateEventLogEntry` | -======= ->>>>>>> 907dd089f ([EDIFIKANA] Mark DS-OCC-008 fixed in datastore test catalog (NONE)) ### 4.8 `SupabaseOrganizationDatastore` (tables: `organizations`, `user_organization_mapping`) From 7dffabc0bf11521b6bd28b49d53abc7cd93973d6 Mon Sep 17 00:00:00 2001 From: Cramsan Date: Sun, 26 Jul 2026 16:10:21 -0700 Subject: [PATCH 3/5] [EDIFIKANA] Add parent-unit guard to createOccupant (NONE) createOccupant had no check that its parent unit was active, unlike createUnit's guard against its own parent Property and (as of this batch) createProperty's guard against its parent Organization (DS-X-002). Mirrors the same shallow, immediate-parent-only fetch-and-throw pattern: fetch the unit filtered deletedAt isExact null, throw NotFoundException if missing/soft-deleted, before proceeding with the original insert. --- .../datastore/supabase/SupabaseOccupantDatastore.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOccupantDatastore.kt b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOccupantDatastore.kt index c797e0162..534221ff9 100644 --- a/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOccupantDatastore.kt +++ b/edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOccupantDatastore.kt @@ -15,6 +15,7 @@ import com.cramsan.edifikana.server.service.models.Occupant import com.cramsan.framework.annotations.BackendDatastore import com.cramsan.framework.core.runSuspendCatching import com.cramsan.framework.logging.logD +import com.cramsan.framework.utils.exceptions.ClientRequestExceptions import io.github.jan.supabase.postgrest.Postgrest import kotlinx.datetime.LocalDate import kotlin.time.Clock @@ -42,6 +43,18 @@ class SupabaseOccupantDatastore(private val postgrest: Postgrest, private val cl ): Result = runSuspendCatching(TAG) { logD(TAG, "Creating occupant for unit: %s", unitId) + + postgrest + .from(UnitEntity.COLLECTION) + .select { + filter { + UnitEntity::unitId eq unitId.unitId + UnitEntity::deletedAt isExact null + } + }.decodeSingleOrNull() ?: throw ClientRequestExceptions.NotFoundException( + "No unit with id $unitId found in database", + ) + val entity = CreateOccupantEntity( unitId = unitId, From 9f6c2e394d2773ee067a441352dc7652a6e91120 Mon Sep 17 00:00:00 2001 From: Cramsan Date: Sun, 26 Jul 2026 16:10:56 -0700 Subject: [PATCH 4/5] [EDIFIKANA] Update DS-X-004 test for createOccupant unit guard (NONE) Rewrites the test to soft-delete the unit directly (matching the shallow, immediate-parent-only guard actually added) instead of its grandparent property, and asserts createOccupant now refuses to create against it. The deeper "unit active, property soft-deleted" scenario is a documented, deliberately deferred gap -- consistent with createUnit not checking its own parent's parent either. --- .../supabase/CrossDatastoreIntegrationTest.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/CrossDatastoreIntegrationTest.kt b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/CrossDatastoreIntegrationTest.kt index a507d4393..4c636422e 100644 --- a/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/CrossDatastoreIntegrationTest.kt +++ b/edifikana/back-end/src/integTest/kotlin/com/cramsan/edifikana/server/datastore/supabase/CrossDatastoreIntegrationTest.kt @@ -111,28 +111,28 @@ class CrossDatastoreIntegrationTest : SupabaseIntegrationTest() { } @Test - fun `DS-X-004 createOccupant against a unit under a soft-deleted property has no active pre-check`(): Unit = + fun `DS-X-004 createOccupant against a soft-deleted unit should fail with NotFoundException`(): Unit = runBlocking { // Arrange val unitId = createTestUnit(propertyId!!, "${testPrefix}_OccupantUnit") - assertTrue(propertyDatastore.deleteProperty(propertyId!!).getOrThrow()) + assertTrue(unitDatastore.deleteUnit(unitId).getOrThrow()) - // Act - unlike Unit/Task's create paths, Occupant's create has no property/unit-active - // pre-check at all; it only references unitId + // Act - createOccupant now guards against this, mirroring createUnit's and + // createProperty's (DS-X-002) existing shallow parent-active checks val result = occupantDatastore.createOccupant( unitId = unitId, userId = null, addedBy = testUserId, - name = "${testPrefix}_OccupantUnderDeletedProperty", + name = "${testPrefix}_OccupantUnderDeletedUnit", email = null, occupantType = OccupantType.TENANT, isPrimary = true, startDate = LocalDate(2026, 1, 1), endDate = null, - ).registerOccupantForDeletion() + ) - // Assert - assertTrue(result.isSuccess) + // Assert - blocked by construction, matching DS-X-001/DS-X-002's precedent + assertTrue(result.isFailure) } @Test From 07a0fa51eac0c9cb09407174bcdf404f18afa6cc Mon Sep 17 00:00:00 2001 From: Cramsan Date: Sun, 26 Jul 2026 16:11:14 -0700 Subject: [PATCH 5/5] [EDIFIKANA] Mark DS-X-004 fixed (shallow case) in datastore test catalog (NONE) --- edifikana/back-end/docs/datastore-test-catalog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edifikana/back-end/docs/datastore-test-catalog.md b/edifikana/back-end/docs/datastore-test-catalog.md index dd3556a35..c6f7fc0a5 100644 --- a/edifikana/back-end/docs/datastore-test-catalog.md +++ b/edifikana/back-end/docs/datastore-test-catalog.md @@ -513,7 +513,7 @@ more datastores — grounded in the actual data relationships observed in the en | DS-X-001 | **P0** | `SupabasePropertyDatastore.deleteProperty` (soft) → `SupabaseUnitDatastore.createUnit(propertyId = )` | The pre-check in `createUnit` filters `deletedAt isExact null` on the property lookup — confirm it correctly throws `NotFoundException`, blocking new units under a soft-deleted property (this is the one place in the module that actively defends against finding #3/#4-style orphaning by construction, at the *child creation* boundary — worth confirming it's not accidentally bypassable) | | DS-X-002 | **P0** | `SupabaseOrganizationDatastore.deleteOrganization` (soft) → `SupabasePropertyDatastore.createProperty(organizationId = )` | Fixed — `createProperty` now checks its parent organization is active, mirroring `createUnit`'s existing guard against its own parent `Property` | | DS-X-003 | P1 | `SupabaseEmployeeDatastore.deleteEmployee` (soft) → `SupabaseTaskDatastore.getTasks(propertyId, assigneeId = )` | Confirm tasks still assigned to a soft-deleted employee remain visible/filterable by that (now-gone) `assigneeId` — nothing in `TaskDatastore` reacts to employee deletion except the explicit, separately-invoked `unassignTasksForEmployee` (DS-TASK-005/006); if a caller forgets to invoke it, stale assignments persist indefinitely and silently | -| DS-X-004 | P1 | `SupabaseOccupantDatastore.createOccupant(unitId = )` | Unlike `Unit`/`Task`'s create paths, `Occupant`'s create has **no property/unit-active pre-check at all** — confirm an occupant can be created against a unit that itself may or may not still be active (Occupant only references `unitId`, never checks it) | +| DS-X-004 | P1 | `SupabaseOccupantDatastore.createOccupant(unitId = )` | Fixed for the unit-itself-soft-deleted case — `createOccupant` now checks its immediate parent `Unit` is active, mirroring `createUnit`/`createProperty`'s (`DS-X-002`) shallow guard pattern. The deeper case (unit active, but its parent property is soft-deleted) remains unguarded, consistent with `createUnit` not checking its own parent's parent either — see the multi-level-ancestor-checks follow-up noted in the `DS-X-004` plan | | DS-X-005 | P0 | `SupabaseMembershipDatastore.removeMember` → `SupabaseOrganizationDatastore.getUserRole` → (separately) `SupabaseMembershipDatastore.getMember` via `v_org_members` | Fixed alongside `DS-ORG-010` — the two read paths now agree post-removal, since `getUserRole` filters `status = ACTIVE` the same way `v_org_members` already did | | DS-X-006 | P1 | `SupabaseRentConfigDatastore.setRentConfig` → `SupabaseUnitDatastore.deleteUnit` (soft) → `SupabaseRentConfigDatastore.getRentConfig(unitId)` | Confirm the rent config row survives the unit's soft-delete unchanged (no cascading soft-delete relationship implemented in application code — any cascade would have to be a DB-level trigger or FK `ON DELETE`, which is out of scope for this Kotlin layer but worth confirming isn't silently relied upon) | | DS-X-007 | P1 | `SupabaseUserDatastore.deleteUser` → `SupabaseNotificationDatastore.getNotificationsForUser(deletedUserId)` | Confirm notifications tied to a soft-deleted user remain queryable by ID (nothing propagates the user's deletion into the notifications table) — consistent with `api-test-catalog.md` XAPI-023's observation about stale notifications, but verifiable here purely at the datastore level without needing the controller/RBAC layer at all |