# Israeli Tax Rates — Seed Data — Implementation Plan

**Spec:** docs/specs/tax-rates-seed-data.md  ·  **Slug:** tax-rates-seed-data  ·  **Wave:** 4
**Depends on:** admin-dashboard

## Goal
Populate the `tax_rates` table (owned by `admin-dashboard`) with the canonical Israeli tax-rate history for `country_code = 'IL'`: corporate income tax (1995→2018+), default withholding tax under Income Tax Ordinance §164 (1995→2012+), and personal income-tax brackets (2015, 2017, 2020, 2022, 2023, 2024, 2025). This seed is the single source of truth for the NULL-rate withholding fallback that `contractor-payouts` and `vendors-suppliers` resolve, and the bracket source that `israeli-tax-reports` / `reports-analytics` read via `getTaxRate`. Delivery is an idempotent SQL seed shipped in `packages/db`, runnable post-migration.

## Architecture
This spec adds **no new table** — `tax_rates` is created by `admin-dashboard`. This task supplies row data only.

Data flow and consumers (all read, never re-seed):
- `getTaxRate(db, countryCode, taxType, date)` (exported by `admin-dashboard` at `packages/db/src/queries/tax.ts`) selects the row with the greatest `effective_from <= date` for a `(country_code, tax_type)` pair. The seed rows are the data this helper returns.
- `withholding_default` row(s) are the fallback withholding rate when a contractor/vendor has no exemption certificate — `contractor-payouts` and `vendors-suppliers` resolve `getTaxRate(db, 'IL', 'withholding_default', billDate)` rather than hard-coding 30%.
- `personal_bracket_1`…`personal_bracket_6` rows feed marginal income-tax banding in `israeli-tax-reports` / `reports-analytics`.

FK constraint: `tax_rates.created_by UUID NOT NULL REFERENCES admin_users.id`. The spec hard-codes `created_by = '00000000-0000-0000-0000-000000000001'` on every row. Therefore the seed MUST first guarantee a system-admin sentinel row in `admin_users` with that exact id (idempotent upsert), otherwise every INSERT violates the FK. `admin_users` is in the locked upstream interface sheet; its columns come from `admin-dashboard`: `id, email, password_hash, totp_secret, role_id, status, created_at`, and `role_id UUID NOT NULL` references `admin_roles`. The sentinel must reference a sentinel `admin_roles` row as well.

Idempotency: every INSERT targets the `UNIQUE (country_code, tax_type, effective_from)` constraint and uses `ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING`, so the seed is safe to re-run and safe to run after manual admin edits (which create new `effective_from` rows, never colliding).

## Tech Stack
- Package: `packages/db` (Drizzle ORM, Neon Postgres via Cloudflare Hyperdrive). Owns schema, migrations, seeds.
- Seed file: plain `.sql` executed by the db package seed runner (`pnpm db:seed`), ordered to run AFTER `drizzle-kit migrate` (the `tax_rates` and `admin_users` tables must already exist).
- No Worker route, no UI, no React — this is data-layer only. The admin UI to view/add rates is built by `admin-dashboard`; this task only fills history.
- Bindings: none at runtime; seed runs through the same Neon connection string `drizzle.config.ts` / the seed runner uses.

## Wave Plan
| Sub-wave | Tasks | Files touched | Parallelizable? |
|----------|-------|---------------|-----------------|
| 4a | Task 1 (sentinel admin), Task 2 (seed SQL) | `packages/db/src/seeds/0001_system_admin_sentinel.sql`, `packages/db/src/seeds/0002_il_tax_rates.sql` | Task 2 depends on Task 1 ordering; author together |
| 4b | Task 3 (seed runner wiring) | `packages/db/src/seeds/run.ts`, `packages/db/package.json` | After 1+2 |
| 4c | Task 4 (verification query / assertions) | `packages/db/src/seeds/verify-tax-rates.sql` | After 3 |

## Tasks

### Task 1: System-admin sentinel rows for FK satisfaction
**Blocks:** 2, 3  ·  **Blocked by:** —
**Files:**
- Create: `packages/db/src/seeds/0001_system_admin_sentinel.sql`
**Steps:**
- [ ] Insert (idempotent) a sentinel `admin_roles` row with a fixed id so the sentinel admin has a valid `role_id` FK target. Use id `00000000-0000-0000-0000-0000000000aa`, name `SYSTEM_SEED`, `permissions = '{}'`, `is_system_role = true`.
- [ ] Insert (idempotent) the sentinel `admin_users` row with the exact id `00000000-0000-0000-0000-000000000001` referenced by every tax-rate seed row. Email `system-seed@zync.is`, a non-login `password_hash` placeholder of `'!'` (no valid hash — this account cannot authenticate), `status = 'suspended'`, `role_id` pointing at the sentinel role.
- [ ] Use `ON CONFLICT (id) DO NOTHING` on both inserts so the file is re-runnable and never clobbers a real edit.
**Schema / Interfaces:**
```sql
-- Sentinel admin role (FK target for admin_users.role_id)
INSERT INTO admin_roles (id, name, permissions, is_system_role)
VALUES ('00000000-0000-0000-0000-0000000000aa', 'SYSTEM_SEED', '{}', true)
ON CONFLICT (id) DO NOTHING;

-- Sentinel admin user — owner of all seeded tax_rates rows (created_by FK).
-- Non-authenticatable: status 'suspended', password_hash is a sentinel that no verifier accepts.
INSERT INTO admin_users (id, email, password_hash, totp_secret, role_id, status, created_at)
VALUES (
  '00000000-0000-0000-0000-000000000001',
  'system-seed@zync.is',
  '!',
  NULL,
  '00000000-0000-0000-0000-0000000000aa',
  'suspended',
  now()
)
ON CONFLICT (id) DO NOTHING;
```
**Acceptance:**
- [ ] After running, `SELECT id FROM admin_users WHERE id = '00000000-0000-0000-0000-000000000001'` returns one row.
- [ ] Re-running the file produces no error and no duplicate rows.
- [ ] The sentinel admin cannot authenticate (status `suspended`, `password_hash = '!'`).

### Task 2: Israeli tax-rate seed SQL (corporate, withholding, brackets)
**Blocks:** 3, 4  ·  **Blocked by:** 1
**Files:**
- Create: `packages/db/src/seeds/0002_il_tax_rates.sql`
**Steps:**
- [ ] Emit three `INSERT INTO tax_rates (...) VALUES ...` blocks: corporate income, default withholding, personal brackets — transcribing every row from the spec verbatim (rates, `effective_from`, `threshold_ils`, Hebrew `notes`).
- [ ] Append `ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING` to every INSERT statement so the seed is idempotent against the `UNIQUE (country_code, tax_type, effective_from)` constraint.
- [ ] Use `gen_random_uuid()` for `id` on every row and the sentinel `'00000000-0000-0000-0000-000000000001'` for `created_by` (matching Task 1).
- [ ] Do NOT emit the 2016 corporate row (it is commented out in the spec — same 0.25 rate as the 2012 row, intentionally omitted; the 2012 row covers 2012–2016).
- [ ] Keep `threshold_ils` NULL exactly where the spec has NULL (all corporate, all withholding, and every `personal_bracket_6`).
- [ ] Preserve the 2025 `personal_bracket_6` rate as `0.50` (47% + 3% surtax combined, threshold NULL) and the 2015 `personal_bracket_5`/`bracket_6` rates as `0.34`/`0.48` exactly as the spec records the structural change (35% bracket introduced in 2017).
**Schema / Interfaces:**
Target table (owned by `admin-dashboard`, reproduced here for the implementer — do NOT create it):
```sql
-- EXISTS already (admin-dashboard). Shown for column reference only.
-- tax_rates (
--   id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
--   country_code TEXT NOT NULL,
--   tax_type TEXT NOT NULL,
--   rate NUMERIC(6,4) NOT NULL,
--   effective_from DATE NOT NULL,
--   threshold_ils NUMERIC,
--   notes TEXT,
--   created_by UUID NOT NULL REFERENCES admin_users(id),
--   created_at TIMESTAMPTZ DEFAULT now(),
--   UNIQUE (country_code, tax_type, effective_from)
-- )
```
Seed body (transcribe exactly):
```sql
-- ============================================================
-- CORPORATE INCOME TAX (מס חברות)
-- Source: Israeli Ministry of Finance / Tax Authority
-- ============================================================
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'corporate_income', 0.36, '1995-01-01', NULL, 'פקודת מס הכנסה — שיעור מס חברות 36%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.34, '2002-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-34%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.31, '2003-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-31%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.29, '2004-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-29%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.27, '2006-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-27%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.29, '2007-01-01', NULL, 'תיקון פקודת מס הכנסה — העלאה ל-29%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.27, '2008-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-27%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.26, '2009-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-26%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.24, '2011-01-01', NULL, 'תיקון פקודת מס הכנסה — הפחתה ל-24%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.25, '2012-01-01', NULL, 'תיקון פקודת מס הכנסה (ועדת טרכטנברג) — העלאה ל-25%', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.24, '2017-01-01', NULL, 'תיקון 245 לפקודת מס הכנסה — הפחתה ל-24% (2017)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'corporate_income', 0.23, '2018-01-01', NULL, 'תיקון 245 לפקודת מס הכנסה — הפחתה ל-23% (2018)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ============================================================
-- DEFAULT WITHHOLDING TAX (ניכוי מס במקור — ברירת מחדל)
-- Source: פקודת מס הכנסה סעיף 164 + תקנות ניכוי מס במקור
-- ============================================================
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'withholding_default', 0.20, '1995-01-01', NULL, 'סעיף 164 פקודת מס הכנסה — שיעור ניכוי ברירת מחדל 20% לחברות', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'withholding_default', 0.30, '2012-01-01', NULL, 'סעיף 164 פקודת מס הכנסה — שיעור ניכוי מס במקור ברירת מחדל 30% לקבלנים/נותני שירות ללא אישור', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ============================================================
-- PERSONAL INCOME TAX BRACKETS (מדרגות מס הכנסה)
-- threshold_ils = income ceiling for that bracket; NULL = top bracket.
-- ============================================================

-- ---- 2015 BRACKETS ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2015-01-01', 63840,  'מדרגת מס 10% — תקרה שנתית 2015', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2015-01-01', 91440,  'מדרגת מס 14% — תקרה שנתית 2015', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2015-01-01', 130800, 'מדרגת מס 20% — תקרה שנתית 2015', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2015-01-01', 216720, 'מדרגת מס 31% — תקרה שנתית 2015', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.34, '2015-01-01', 496920, 'מדרגת מס 34% — תקרה שנתית 2015', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.48, '2015-01-01', NULL,   'מדרגת מס 48% — ללא תקרה 2015 (כולל היטל 3% על הכנסה מעל ~640k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2017 BRACKETS (35% bracket introduced, 34% removed) ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2017-01-01', 73560,  'מדרגת מס 10% — תקרה שנתית 2017', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2017-01-01', 103920, 'מדרגת מס 14% — תקרה שנתית 2017', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2017-01-01', 167280, 'מדרגת מס 20% — תקרה שנתית 2017', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2017-01-01', 239760, 'מדרגת מס 31% — תקרה שנתית 2017', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2017-01-01', 498360, 'מדרגת מס 35% — תקרה שנתית 2017 (מדרגה חדשה)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.47, '2017-01-01', NULL,   'מדרגת מס 47% — ללא תקרה 2017 (+ היטל 3% = 50% על הכנסה מעל ~651k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2020 BRACKETS ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2020-01-01', 75480,  'מדרגת מס 10% — תקרה שנתית 2020', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2020-01-01', 108360, 'מדרגת מס 14% — תקרה שנתית 2020', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2020-01-01', 173880, 'מדרגת מס 20% — תקרה שנתית 2020', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2020-01-01', 241680, 'מדרגת מס 31% — תקרה שנתית 2020', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2020-01-01', 502920, 'מדרגת מס 35% — תקרה שנתית 2020', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.47, '2020-01-01', NULL,   'מדרגת מס 47% — ללא תקרה 2020 (+ היטל 3% = 50% על הכנסה מעל ~651k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2022 BRACKETS ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2022-01-01', 77400,  'מדרגת מס 10% — תקרה שנתית 2022', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2022-01-01', 110880, 'מדרגת מס 14% — תקרה שנתית 2022', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2022-01-01', 178080, 'מדרגת מס 20% — תקרה שנתית 2022', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2022-01-01', 247440, 'מדרגת מס 31% — תקרה שנתית 2022', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2022-01-01', 514920, 'מדרגת מס 35% — תקרה שנתית 2022', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.47, '2022-01-01', NULL,   'מדרגת מס 47% — ללא תקרה 2022 (+ היטל 3% = 50% על הכנסה מעל ~663k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2023 BRACKETS ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2023-01-01', 81480,  'מדרגת מס 10% — תקרה שנתית 2023', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2023-01-01', 116760, 'מדרגת מס 14% — תקרה שנתית 2023', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2023-01-01', 187440, 'מדרגת מס 20% — תקרה שנתית 2023', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2023-01-01', 260520, 'מדרגת מס 31% — תקרה שנתית 2023', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2023-01-01', 542160, 'מדרגת מס 35% — תקרה שנתית 2023', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.47, '2023-01-01', NULL,   'מדרגת מס 47% — ללא תקרה 2023 (+ היטל 3% = 50% על הכנסה מעל ~698k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2024 BRACKETS ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2024-01-01', 83040,  'מדרגת מס 10% — תקרה שנתית 2024', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2024-01-01', 118800, 'מדרגת מס 14% — תקרה שנתית 2024', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2024-01-01', 190800, 'מדרגת מס 20% — תקרה שנתית 2024', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2024-01-01', 265320, 'מדרגת מס 31% — תקרה שנתית 2024', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2024-01-01', 553800, 'מדרגת מס 35% — תקרה שנתית 2024', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.47, '2024-01-01', NULL,   'מדרגת מס 47% — ללא תקרה 2024 (+ היטל 3% = 50% על הכנסה מעל ~712k)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;

-- ---- 2025 BRACKETS (CURRENT — verified PwC Tax Summaries 2025) ----
INSERT INTO tax_rates (id, country_code, tax_type, rate, effective_from, threshold_ils, notes, created_by) VALUES
(gen_random_uuid(), 'IL', 'personal_bracket_1', 0.10, '2025-01-01', 84120,  'מדרגת מס 10% — תקרה שנתית 2025 (מאומת PwC)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_2', 0.14, '2025-01-01', 120720, 'מדרגת מס 14% — תקרה שנתית 2025 (מאומת PwC)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_3', 0.20, '2025-01-01', 193800, 'מדרגת מס 20% — תקרה שנתית 2025 (מאומת PwC)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_4', 0.31, '2025-01-01', 269280, 'מדרגת מס 31% — תקרה שנתית 2025 (מאומת PwC)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_5', 0.35, '2025-01-01', 560280, 'מדרגת מס 35% — תקרה שנתית 2025 (מאומת PwC)', '00000000-0000-0000-0000-000000000001'),
(gen_random_uuid(), 'IL', 'personal_bracket_6', 0.50, '2025-01-01', NULL,   'מדרגת מס 47% + היטל 3% = 50% — ללא תקרה 2025 (על הכנסה מעל 721,560 ₪; מאומת PwC)', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (country_code, tax_type, effective_from) DO NOTHING;
```
**Acceptance:**
- [ ] `SELECT count(*) FROM tax_rates WHERE country_code='IL' AND tax_type='corporate_income'` returns 12.
- [ ] `SELECT count(*) FROM tax_rates WHERE country_code='IL' AND tax_type='withholding_default'` returns 2.
- [ ] `SELECT count(*) FROM tax_rates WHERE country_code='IL' AND tax_type LIKE 'personal_bracket_%'` returns 42 (7 years × 6 brackets).
- [ ] Total seeded `tax_rates` rows for `IL` = 56.
- [ ] No row has `country_code <> 'IL'`; every `created_by = '00000000-0000-0000-0000-000000000001'`.
- [ ] Re-running the file inserts zero additional rows (idempotent via `ON CONFLICT DO NOTHING`).

### Task 3: Wire the seed into the db package seed runner
**Blocks:** 4  ·  **Blocked by:** 1, 2
**Files:**
- Modify: `packages/db/src/seeds/run.ts`
- Modify: `packages/db/package.json`
**Steps:**
- [ ] In the seed runner (`run.ts`), execute the `.sql` files from `packages/db/src/seeds/` in lexical order so `0001_system_admin_sentinel.sql` runs before `0002_il_tax_rates.sql` (the FK target must exist first). Read each file, split into statements on the statement terminator, and execute over the Neon connection the runner already opens.
- [ ] Ensure the runner runs AFTER migrations: document that `pnpm db:seed` requires `pnpm db:migrate` to have created `admin_users`, `admin_roles`, and `tax_rates`. If `run.ts` already exists from foundation, only append the new files to its ordered list; do not rewrite its connection logic.
- [ ] Add/confirm a `db:seed` script in `packages/db/package.json` that invokes `run.ts`.
- [ ] The runner must be transactional per file (BEGIN/COMMIT) so a partial failure rolls back; `ON CONFLICT DO NOTHING` keeps re-runs clean.
**Schema / Interfaces:**
```ts
// packages/db/src/seeds/run.ts — ordered seed application
const SEED_FILES = [
  '0001_system_admin_sentinel.sql',
  '0002_il_tax_rates.sql',
] as const;
```
```json
// packages/db/package.json (scripts excerpt)
{ "scripts": { "db:seed": "tsx src/seeds/run.ts" } }
```
**Acceptance:**
- [ ] `pnpm --filter @zync/db db:seed` against a freshly migrated Neon branch exits 0.
- [ ] Running `db:seed` twice in a row produces identical row counts (no duplicates, no FK error).

### Task 4: Seed verification assertions
**Blocks:** —  ·  **Blocked by:** 3
**Files:**
- Create: `packages/db/src/seeds/verify-tax-rates.sql`
**Steps:**
- [ ] Write a verification SQL script the implementer runs post-seed that asserts row counts and the two load-bearing lookups consumers depend on.
- [ ] Assert `getTaxRate`-equivalent resolution: latest `withholding_default` effective on `2026-06-02` is `0.30`; latest `corporate_income` is `0.23`; latest 2025 `personal_bracket_6` is `0.50`.
- [ ] Assert the pre-2012 withholding fallback resolves to `0.20` for a `2010-01-01` date (proving the historical fallback works for back-dated bills in `contractor-payouts` / `vendors-suppliers`).
**Schema / Interfaces:**
```sql
-- Latest withholding default (consumers' NULL-rate fallback) must be 30% as of today.
SELECT rate FROM tax_rates
WHERE country_code='IL' AND tax_type='withholding_default' AND effective_from <= DATE '2026-06-02'
ORDER BY effective_from DESC LIMIT 1;   -- expect 0.3000

-- Back-dated withholding fallback must be 20% for a 2010 bill.
SELECT rate FROM tax_rates
WHERE country_code='IL' AND tax_type='withholding_default' AND effective_from <= DATE '2010-01-01'
ORDER BY effective_from DESC LIMIT 1;   -- expect 0.2000

-- Latest corporate income tax.
SELECT rate FROM tax_rates
WHERE country_code='IL' AND tax_type='corporate_income' AND effective_from <= DATE '2026-06-02'
ORDER BY effective_from DESC LIMIT 1;   -- expect 0.2300

-- Current top personal bracket (47% + 3% surtax modelled as 50%).
SELECT rate FROM tax_rates
WHERE country_code='IL' AND tax_type='personal_bracket_6' AND effective_from <= DATE '2026-06-02'
ORDER BY effective_from DESC LIMIT 1;   -- expect 0.5000

-- Total seeded IL rows.
SELECT count(*) FROM tax_rates WHERE country_code='IL';   -- expect 56
```
**Acceptance:**
- [ ] Each query returns the documented expected value.
- [ ] `getTaxRate(db, 'IL', 'withholding_default', new Date('2026-06-02'))` (admin-dashboard helper) returns `0.3`.
- [ ] `getTaxRate(db, 'IL', 'withholding_default', new Date('2010-01-01'))` returns `0.2`.
