- Non-active statuses (pending/suspended/expired/deleted) now uniformly:
disable AList user + clear role scopes (soft, keeps user/role/CP/history);
no-op when the customer has no AList user yet (pure NocoDB label).
- Resume transitions (X -> active) recompute effective grants from current
CustomerProducts + expiry, never restore stale scopes.
- businessChanged() now also treats after-insert (new row, no previous) as a
business change so fresh customers are processed on first webhook.
- Lifecycle test covers the full matrix: 10 scenarios, all green.
- AccessGrants still not materialized (deferred to W4 per decision).
- Customers.status stays account-level, outside permission calc.
- 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.
- Replace brittle sync_status==='synced' loop guard with NocoDB v2
old/new row comparison (previous_rows vs rows).
- businessChanged() = business field move OR sync_required set; echo
(bookkeeping-only delta) -> skip_writeback_echo, zero AList mutation.
- writeBack() value-change guard: GET live record, PATCH only fields
whose value actually changes (no-change -> no PATCH -> no webhook).
- sync_required Checkbox honored as force-reprocess flag, cleared on write-back.
- Extract-id node passes previousRow/newRow through Load Env to lifecycle.
- W1 lifecycle test now covers echo suppression (26 assertions).
- Customers.status still NOT consumed by permission calc (TBD semantics).
- W2/W3 effective-grants semantics unchanged.
- 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.
AList expects bare token (not 'Bearer <token>'). With 'Bearer' prefix,
all role/get calls returned non-data responses -> currentPaths degraded
to [] -> W3 kept re-applying same scopes every run (false toAdd) and
never detected expired scopes (false toRemove). This is the root cause
of W3 not showing hoe's expiry removal.
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.