Per-association chart of accounts

Each association now owns an independent set of chart_of_accounts rows. Two
associations can both have a "5000" meaning different things, edited
independently — previously buildium rows were shared across associations via the
association_ids[] array, so editing one association's number edited it for all.

- Data migration: split each shared buildium row into one row per association in
  its association_ids (excluding nulls and ids of deleted associations). The
  original row stays as the per-association row for its own association_id;
  clones are added for the others — nothing is deleted, so no FK dangles.
  94 -> 370 buildium rows. References repointed by each record's association
  (bills, budgets, owner_ledger_entries, vendor_coa_mappings, units, vendors,
  journal_entries; budget_actuals_monthly is a view). parent_account_id remapped
  same-association; orphan-parent children become top-level. Pristine backup in
  public._coa_perassoc_backup. Ran in one transaction with in-line verification.
- Uniqueness: drop (account_number, accounting_system); add
  UNIQUE(association_id, account_number) WHERE accounting_system <> 'platform'
  (platform rows mirror accounting.accounts and carry blank/dup codes). This also
  finally backs the buildium importers' existing onConflict target.
- Keep association_ids as a single-element mirror of association_id during the
  transition so the admin COA page and direct array-contains callers keep working.
- App: fetchChartOfAccounts scopes buildium/zoho by association_id when an
  association is given (was system-wide). Importers and sync_account_to_public_coa
  were already per-association; no change needed.

Verified on live data: 370 singleton-array rows across 12 associations, zero
intra-association duplicates, zero cross-association references or parents, and a
live two-association "5000" independence test (create/rename/isolate) passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 13:38:50 -04:00
parent 8f1cbcd3af
commit 6634907799
2 changed files with 150 additions and 4 deletions
+10 -4
View File
@@ -54,7 +54,10 @@ function fromPlatform(row: any, associationId: string): NormalizedAccount {
* - `platform` → the Accounting module's `accounting.accounts` (single source of
* truth once an association is on the platform). Returns [] if the association
* has no `accounting.companies` row yet.
* - `zoho` / `buildium` → the public `chart_of_accounts`, scoped by system.
* - `zoho` / `buildium` → the public `chart_of_accounts`, scoped by system AND, when
* an association is given, by that association. Each association owns an independent
* set of accounts (one row per association — see the per-association COA migration),
* so two associations can both have a "5000" meaning different things.
*
* Returned rows are normalized to {@link NormalizedAccount} so callers never
* branch on the source.
@@ -81,11 +84,14 @@ export async function fetchChartOfAccounts(
return (data ?? []).map((row) => fromPlatform(row, associationId));
}
const { data, error } = await supabase
let query = supabase
.from("chart_of_accounts")
.select("*")
.eq("accounting_system", system)
.order("account_number", { ascending: true });
.eq("accounting_system", system);
// Scope to the current association so each one sees only its own accounts. When no
// association is supplied, fall back to the whole system set (back-compat).
if (associationId) query = query.eq("association_id", associationId);
const { data, error } = await query.order("account_number", { ascending: true });
if (error) throw error;
return (data ?? []).map(fromPublic);
}