Bite #30F — Collaborator Membership Cutover - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki

Bite #30F — Collaborator Membership Cutover

Manual Promotion Verification

Issue branch: 719-bite-30f-collaborator-membership-cutover
Latest supplied source snapshot used to ground this checklist: c9455b47f528108795c9af8c1724311b63aed1e4

Your current HEAD may be newer if you committed the final local build/test fixes after that snapshot. Record the actual SHA you promote in the Promotion Record at the end.

Canonical relationship under test

Global Person
  -> Person–Tenant Membership
      -> zero or more historical Collaborator Journeys
      -> at most one open Collaborator Journey

Bite 30F must make Person–Tenant Membership the prerequisite and enduring parent of Collaborator Journey while retaining the legacy Person projection only for staged compatibility. Expense, Earnings, and Ledger ownership remain deferred to Bite 30G.


1. Required companion files

Use:

seed-bite30f-manual-test-identities.py
Bite-30F-DevTools.js
Bite-30F-Manual-Verification.sql

The 30F seed script reuses the repository's existing:

scripts/seed-bite30e-manual-test-identities.py

2. Prepare the deterministic identities

2.1 Stop the backend

The fixture generator writes directly to SQLite.

2.2 Run from the ERS repository root

python3 ~/Downloads/seed-bite30f-manual-test-identities.py

If the 30f batch already exists, use a fresh batch:

python3 ~/Downloads/seed-bite30f-manual-test-identities.py --batch 30f2

The SQL companion is preconfigured for batch 30f. If you use another batch, replace .30f. in the four fixture logins in the SQL file.

2.3 Default credentials

Identity Login Password Purpose
A [email protected] Manual-30C-Password! Multi-Tenant ordinary Account
B [email protected] Manual-30C-Password! Tenant A lifecycle Account
C [email protected] Manual-30C-Password! Tenant B INACTIVE-Membership control
D [email protected] Manual-30C-Password! Tenant Administrator in Tenant A and Tenant B

Use Identity D for administrative UI/API work.

Save the seed output. It prints the exact Authentication Account ID, Person ID, Tenant ID, Actor ID, Actor Key, Membership ID, and Legacy Person ID needed below.


3. Load the DevTools helper

  1. Start the application.
  2. Sign in as Identity D.
  3. Open DevTools → Console.
  4. Paste the entire contents of Bite-30F-DevTools.js.

Expected:

ERS30F helper loaded.

Useful calls:

await ERS30F.currentActor()
await ERS30F.candidates()
await ERS30F.createByMembership("MEMBERSHIP_ID", "notes")
await ERS30F.createByLegacyPerson("LEGACY_PERSON_ID", "notes")

The helper reads the currently selected Tenant and dynamically chooses valid active reference-data values, so no hard-coded payment/sector/location/task/status IDs are required.


[x] Test 1 — Fixture Shape and Migration 000056 Integrity

Purpose

Verify the database is in a clean 30F state before changing manual-test data.

Steps

From the repository root:

sqlite3 -header -column backend/data/app.db

At the SQLite prompt:

.read ~/Downloads/Bite-30F-Manual-Verification.sql

Expected fixture shape

  • Identity A: two ACTIVE Memberships and two ACTIVE Tenant Actors; no delegated Roles.
  • Identity B: one ACTIVE Membership and one ACTIVE Tenant Actor; no delegated Roles.
  • Identity C:
    • Authentication Account ACTIVE;
    • Tenant Actor ACTIVE;
    • Person–Tenant Membership INACTIVE;
    • legacy Person still ACTIVE;
    • Global Person profile COMPLETE and can_create_collaborator = 1.
  • Identity D: two ACTIVE Memberships and two ACTIVE Tenant Actors.
  • Identity D has TENANT_ADMIN in both Tenants.

Expected migration integrity

journeys_without_membership = 0
mismatched_journeys         = 0

The duplicate-open-Journey query returns no rows.

PRAGMA foreign_key_check returns no rows.

Result

  • PASS
  • FAIL

[x] Test 2 — Candidate Discovery Is Membership-Native

[x] Part A — ACTIVE Membership is eligible

  1. Sign in as Identity D.
  2. Select Tenant A — Byte 28A Manual Test.
  3. Navigate to /collaborators/new.
  4. Search for Identity B.
  5. Confirm Identity B appears.
  6. In DevTools:
await ERS30F.candidates()

Find Identity B.

Expected Identity B candidate

[x] membershipId          = Identity B Membership ID
[x] globalPersonId        = Identity B Person ID
[x] id                    = Identity B Legacy Person ID

[x] Part B — INACTIVE Membership is not eligible even though legacy Person is ACTIVE

  1. Switch to Tenant B — default.
  2. Navigate to /collaborators/new.
  3. Search for Identity C.

Expected

[x] Identity C is not offered.

Run:

await ERS30F.candidates()

[x] Identity C's Membership ID must not appear.

This deliberately proves candidate eligibility follows the Membership, not the ACTIVE legacy Person row.

Result

  • PASS
  • FAIL

Test 3 — Create a Journey Through the Canonical Membership

Starting state

Identity B has an ACTIVE Tenant A Membership and no open Journey.

Steps

  1. As Identity D, select Tenant A.
  2. Open /collaborators/new.
  3. Search for Identity B.
  4. Select Identity B.
  5. Verify the page says:
[x] Create a Collaborator Journey from an active Person–Tenant Membership.
  1. Complete the required fields using ACTIVE choices.
  2. Use:
Payment Value: 210.25
Notes: 30F manual B1
  1. Click Create Collaborator.
  2. Open the resulting Collaborator Journey.

Expected Person Summary

[x] Person ID        = Identity B Person ID = global_people.id
[x] Membership ID    = Identity B Membership ID = person_tenant_memberships.id
[x] Legacy Person ID = Identity B Legacy Person ID = people.id

The three identifiers must retain their distinct meanings.

SQL verification

Re-run:

.read ~/Downloads/Bite-30F-Manual-Verification.sql

The row 30F manual B1 must show:

[x] membership_id    = Identity B Membership ID
[x] global_person_id = Identity B Person ID
[x] legacy_person_id = Identity B Legacy Person ID
[x] closed_at        = NULL

Result

  • PASS
  • FAIL

Test 4 — A Membership Cannot Have Two Open Journeys

  1. Remain in Tenant A.
  2. Return to /collaborators/new.
  3. Search for Identity B.

Expected UI

Identity B is not eligible while B1 is open.

API check

Insert Identity B Membership ID:

await ERS30F.createByMembership(
  "IDENTITY_B_MEMBERSHIP_ID",
  "30F duplicate B1 attempt"
)

Expected

HTTP 400

Validation is on membershipId and states:

Membership already has an open Collaborator Journey

No second open Journey is created.

Result

  • PASS
  • FAIL

Test 5 — Same-Tenant Membership Is Enforced

  1. As Identity D, select Tenant A.
  2. Copy Identity A Tenant B Membership ID from seed output.
  3. Run:
await ERS30F.createByMembership(
  "IDENTITY_A_TENANT_B_MEMBERSHIP_ID",
  "30F cross-tenant membership rejection"
)

Expected

HTTP 400

The error states that an ACTIVE Person–Tenant Membership in the selected Tenant is required.

No Journey is created.

Result

  • PASS
  • FAIL

Test 6 — Same Global Person Can Collaborate Independently in Two Tenants

Part A — Tenant A canonical creation

  1. Select Tenant A.
  2. Open /collaborators/new.
  3. Search for Identity A.
  4. Create:
Payment Value: 225.00
Notes: 30F manual A Tenant A
  1. Open the Journey.

Expected:

Membership ID = Identity A Tenant A Membership ID
Person ID     = Identity A Person ID

Record the Journey ID. 30E Identity A 30f

Part B — Tenant B remains eligible

  1. Switch to Tenant B.
  2. Open /collaborators/new.
  3. Search for Identity A.

Expected: Identity A is still eligible because the Tenant B Membership is distinct.

Part C — Exercise legacy compatibility selector

Copy Identity A Tenant B Legacy Person ID.

Run:

await ERS30F.createByLegacyPerson(
  "IDENTITY_A_TENANT_B_LEGACY_PERSON_ID",
  "30F manual A Tenant B legacy selector"
)

Expected

HTTP 201

Response:

[x] membershipId   = Identity A Tenant B Membership ID
[x] personId       = Identity A Person ID
[x] legacyPersonId = Identity A Tenant B Legacy Person ID

The legacy personId request value acted only as a selector; the persisted canonical parent is the Membership.

SQL verification

Re-run the SQL companion.

Identity A must have two open rows for the same Global Person:

Tenant A -> Tenant A Membership -> Tenant A Journey
Tenant B -> Tenant B Membership -> Tenant B Journey

Result

  • PASS
  • FAIL

Test 7 — Current Collaborator Resolution Follows Selected Membership

Starting state

Identity A has one open Journey in each Tenant.

  1. Sign out Identity D.
  2. Sign in as Identity A:
[email protected]
Manual-30C-Password!
  1. Select Tenant A.
  2. Load Bite-30F-DevTools.js.
  3. Run:
await ERS30F.currentActor()

Expected Tenant A:

globalPersonId = Identity A Person ID
membershipId   = Identity A Tenant A Membership ID
collaboratorId = Identity A Tenant A Journey ID
  1. Switch to Tenant B.
  2. Run again:
await ERS30F.currentActor()

Expected Tenant B:

globalPersonId = same Identity A Person ID
membershipId   = Identity A Tenant B Membership ID
collaboratorId = Identity A Tenant B Journey ID

No Journey may be borrowed across Tenants.

Result

  • PASS
  • FAIL

Test 8 — Closing a Journey Ends Collaboration, Not Identity

Starting state

Identity B has open B1.

  1. Sign out Identity A.
  2. Sign in as Identity D.
  3. Select Tenant A.
  4. Open the Journey with notes 30F manual B1.
  5. In Journey Settlement, verify Can close = Yes.

If a positive balance prevents closure, use the existing settlement workflow until Can close = Yes.

  1. Click Close Journey.
  2. Click Confirm reauthentication.
  3. Select:
End-of-journey settlement
  1. Enter:
Reason text:
Bite 30F manual lifecycle verification

Notes:
Closing B1 to verify Membership continuity
  1. If required, choose a different eligible second approver.
  2. Click Close Journey.

Expected

Journey closed successfully.

Closed At becomes non-empty.

SQL verification

Re-run the SQL companion.

Identity B must show:

Authentication Account active = 1
Tenant Actor active            = 1
Membership status              = ACTIVE
B1 closed_at                   = non-null

Authentication/current Actor verification

  1. Sign out Identity D.
  2. Sign in as Identity B.
  3. Select Tenant A.
  4. Load the DevTools helper.
  5. Run:
await ERS30F.currentActor()

Expected:

globalPersonId = Identity B Person ID
membershipId   = Identity B Membership ID
collaboratorId = absent/empty

Identity B still authenticates and retains base Person self-service.

Result

  • PASS
  • FAIL

Test 9 — A Later Journey Can Reuse the Same Membership

Starting state

Identity B Membership = ACTIVE
B1 = CLOSED
No open Journey
  1. Sign out Identity B.
  2. Sign in as Identity D.
  3. Select Tenant A.
  4. Open /collaborators/new.
  5. Search for Identity B.

Expected: Identity B is eligible again.

  1. Create:
Payment Value: 275.00
Notes: 30F manual B2
  1. Open B2.

Expected:

B2 Journey ID != B1 Journey ID
Membership ID = same Identity B Membership ID
Person ID = same Identity B Person ID
Legacy Person ID = same Identity B Legacy Person ID

SQL verification

Identity B must have:

B1 -> same Membership -> closed_at non-null
B2 -> same Membership -> closed_at NULL

Result

  • PASS
  • FAIL

Test 10 — Database Identity and History Protections

Open:

sqlite3 -header -column backend/data/app.db

Run each statement one at a time.

10.1 Membership ID immutable

UPDATE collaborator_journeys
SET membership_id = 'invalid-membership-id'
WHERE notes = '30F manual B2';

Expected failure:

collaborator_membership_identity_immutable

10.2 Tenant ID immutable

UPDATE collaborator_journeys
SET tenant_id = 'invalid-tenant-id'
WHERE notes = '30F manual B2';

Expected failure:

collaborator_membership_identity_immutable

10.3 Legacy Person compatibility ID immutable

UPDATE collaborator_journeys
SET person_id = 'invalid-legacy-person-id'
WHERE notes = '30F manual B2';

Expected failure:

collaborator_membership_identity_immutable

10.4 Membership history protected

DELETE FROM person_tenant_memberships
WHERE id = (
  SELECT c.membership_id
  FROM collaborator_journeys c
  WHERE c.notes = '30F manual B2'
  LIMIT 1
);

Expected failure:

collaborator_membership_history_protected

Final check

PRAGMA foreign_key_check;

Expected: no rows.

Re-run the SQL companion and confirm B2 still has its original Tenant ID, Membership ID, and Legacy Person ID.

Result

  • PASS
  • FAIL

Test 11 — Downstream Financial Compatibility Remains Intact

Purpose

Confirm 30F did not perform Bite 30G early.

  1. As Identity D in Tenant A, open B2.
  2. Click Current Account.
  3. Verify it loads successfully.
  4. Return to B2.
  5. Navigate to Expenses.
  6. Create a small disposable Expense for B2 using the normal UI, or open an existing B2 Expense created for this test.
  7. Return to B2 Current Account.

Expected

  • Current Account resolves to B2.
  • Expense workflow resolves the active Collaborator Journey correctly.
  • No cross-Tenant data appears.
  • No UI/API behavior treats Person–Tenant Membership as the new financial owner.
  • No unexpected 403, 404, 500, or identity mismatch occurs.

Result

  • PASS
  • FAIL

Test 12 — Final 30F Integrity Recheck

Run again:

sqlite3 -header -column backend/data/app.db

Then:

.read ~/Downloads/Bite-30F-Manual-Verification.sql

Expected

journeys_without_membership = 0
mismatched_journeys         = 0
duplicate open Journey query = no rows
PRAGMA foreign_key_check     = no rows

Also verify:

  • Identity A has one open Journey per Tenant Membership.
  • Identity B has one closed Journey and one open Journey under the same Membership.
  • Identity C remains Membership-INACTIVE and has no Journey.
  • Identity D remains Tenant Administrator in both Tenants.

Result

  • PASS
  • FAIL

Final Promotion Gate

  • Migration 000056 has no unmapped Journeys.
  • Journey/Membership/Tenant backfill relationships are consistent.
  • No Membership has more than one open Journey.
  • ACTIVE same-Tenant Membership is eligible.
  • INACTIVE Membership is ineligible even when legacy Person is ACTIVE and complete.
  • New Journey persists Membership as canonical parent.
  • Collaborator API personId is Global Person ID.
  • Collaborator API legacyPersonId is temporary people.id.
  • Open Journey makes only that Membership ineligible.
  • Cross-Tenant Membership borrowing is rejected.
  • Same Global Person can collaborate independently through different Tenant Memberships.
  • Legacy personId resolves to the canonical same-Tenant Membership.
  • Current Actor resolves the Journey belonging to the selected Membership.
  • Closing Journey preserves Person.
  • Closing Journey preserves Membership.
  • Closing Journey preserves Authentication Account.
  • Closing Journey preserves Tenant Actor.
  • Closed Journey removes current Collaborator identity/capabilities.
  • Later Journey can reuse same Membership.
  • Journey Tenant/Membership/Legacy-Person identity is immutable.
  • Membership history referenced by Journeys cannot be deleted.
  • Current Account compatibility still works.
  • Expense compatibility still works.
  • No cross-Tenant data exposure occurred.
  • No unexpected browser-console errors occurred.
  • No unexpected backend errors occurred.
  • Final PRAGMA foreign_key_check returns no rows.

Promotion Record

Result: PASS / FAIL

Tester:
________________________________________

Date:
________________________________________

Branch:
719-bite-30f-collaborator-membership-cutover

Promoted SHA:
________________________________________

Notes:
________________________________________
________________________________________
________________________________________