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
- Start the application.
- Sign in as Identity D.
- Open DevTools → Console.
- 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_ADMINin 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
- Sign in as Identity D.
- Select Tenant A — Byte 28A Manual Test.
- Navigate to
/collaborators/new. - Search for Identity B.
- Confirm Identity B appears.
- 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
- Switch to Tenant B — default.
- Navigate to
/collaborators/new. - 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
- As Identity D, select Tenant A.
- Open
/collaborators/new. - Search for Identity B.
- Select Identity B.
- Verify the page says:
[x] Create a Collaborator Journey from an active Person–Tenant Membership.
- Complete the required fields using ACTIVE choices.
- Use:
Payment Value: 210.25
Notes: 30F manual B1
- Click Create Collaborator.
- 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
- Remain in Tenant A.
- Return to
/collaborators/new. - 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
- As Identity D, select Tenant A.
- Copy Identity A Tenant B Membership ID from seed output.
- 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
- Select Tenant A.
- Open
/collaborators/new. - Search for Identity A.
- Create:
Payment Value: 225.00
Notes: 30F manual A Tenant A
- 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
- Switch to Tenant B.
- Open
/collaborators/new. - 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.
- Sign out Identity D.
- Sign in as Identity A:
[email protected]
Manual-30C-Password!
- Select Tenant A.
- Load
Bite-30F-DevTools.js. - 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
- Switch to Tenant B.
- 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.
- Sign out Identity A.
- Sign in as Identity D.
- Select Tenant A.
- Open the Journey with notes
30F manual B1. - In Journey Settlement, verify
Can close = Yes.
If a positive balance prevents closure, use the existing settlement workflow until Can close = Yes.
- Click Close Journey.
- Click Confirm reauthentication.
- Select:
End-of-journey settlement
- Enter:
Reason text:
Bite 30F manual lifecycle verification
Notes:
Closing B1 to verify Membership continuity
- If required, choose a different eligible second approver.
- 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
- Sign out Identity D.
- Sign in as Identity B.
- Select Tenant A.
- Load the DevTools helper.
- 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
- Sign out Identity B.
- Sign in as Identity D.
- Select Tenant A.
- Open
/collaborators/new. - Search for Identity B.
Expected: Identity B is eligible again.
- Create:
Payment Value: 275.00
Notes: 30F manual B2
- 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.
- As Identity D in Tenant A, open B2.
- Click Current Account.
- Verify it loads successfully.
- Return to B2.
- Navigate to Expenses.
- Create a small disposable Expense for B2 using the normal UI, or open an existing B2 Expense created for this test.
- 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
000056has 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
personIdis Global Person ID. - Collaborator API
legacyPersonIdis temporarypeople.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
personIdresolves 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_checkreturns no rows.
Promotion Record
Result: PASS / FAIL
Tester:
________________________________________
Date:
________________________________________
Branch:
719-bite-30f-collaborator-membership-cutover
Promoted SHA:
________________________________________
Notes:
________________________________________
________________________________________
________________________________________