- calculateEffectiveGrants queried CustomerProducts/AccessGrants with
(customer,eq,N), which matches the link field's DISPLAY NAME and always
returns 0 rows -> desiredPaths always [] -> W1/W2/W3 write empty scopes.
- Regression from the shared-lib refactor (3ecee30): W2 had already fixed
this in 36a6b3b but the shared extract reverted to the old syntax.
- Correct syntax (customer.Id,eq,N) verified against live NocoDB for
customer 6 -> /Class/Health, /Class/Other, /Template/Music.
- Updated all three test mocks to match the corrected where clause.
- Extract calculateEffectiveGrants into n8n/shared/effective-grants.js
(single source of truth), inlined at build time into W1/W2/W3 via
gen_w*.js + inline_shared.js — removes the duplicated algorithm.
- W1 now provisions with a status branch:
suspended/deleted -> disable AList user + clear role scopes (soft delete)
active -> create/update user+role, RECOMPUTE desired scopes (never restore
stale), re-enable
- Loop guard corrected: check AList user LIVE disabled state via
GET /api/admin/user/get (sync_status alone breaks suspend->resume).
- W2/W3 rebuilt on shared lib (identical behaviour).
- Tests: test_effective_grants.js (15), test_w3_refactored.js (13),
test_w1_lifecycle.js (23) — all green.
- Drop superseded w2-compute.js / w3-compute.js / test_w3.js.
Root cause: expires_at == today was NOT filtered (used '< today'), so a
product expiring ON today kept its resources in desired scopes -> no diff
-> no removal. Changed to '<='. Applied to W3 (CP + grant loops) and W2
(active filter). Also W2 previously had NO expiry filter on active CPs at
all.
When CP record is drafted with product set before customer,
the webhook fires with customerId=null -> now returns
skipWriteBack=true (routed to No-op) instead of throwing.
Record re-fires webhook properly once customer field is set.