# DepreCore

DepreCore is a fixed asset management and depreciation platform for small and mid-sized organizations. This build includes an Express API, Vue/Vuetify frontend, SQLite persistence, role-aware login, asset register, templates, JSON tax-rule sets, depreciation reports, import/export, and admin configuration.

Each asset carries six independent books (GAAP, Federal, State, AMT, ACE, User-defined), each with its own cost, depreciation method, useful life, §179 amount, bonus percentage, and convention. The asset workbench also includes a File Cabinet (invoices, photos, manuals), warranty/service-agreement tracking, a per-asset task list, and a running notes timeline.

The **Books** screen manages the sets of books as first-class entities: create user-defined books, enable/disable each book, set its type (financial/tax/internal) and entry defaults, and assign which tax rule set drives its depreciation. Enabled depreciation books automatically apply to assets (new and existing), appear in the asset's Books tab, and are selectable in reports — the engine resolves rules per book rather than from a single global set. It also renders each book in a general-ledger table (asset cost, accumulated depreciation, and period expense rolled up to GL accounts with debit/credit and net book value), with PDF/Excel/CSV export.

Disposals support full and partial dispositions with automatic gain/loss against book net-book-value (with a live preview before committing), Form 4797 property typing, and impairment/write-down adjustments. Lease accounting captures each agreement and generates an ASC 842-style amortization schedule (present value, periodic interest/principal, lease-liability and right-of-use balances).

Barcode tracking lets you print Code 128 / Code 39 labels (single asset from the workbench, or a batch from the register) and includes a **Scan** screen: use a device camera to scan an asset's barcode (or type the code), then update its status, location, department, custodian, condition, and add a field note on the spot. Camera scanning requires HTTPS or localhost; manual code entry works everywhere.

The **Maintenance** screen builds reusable preventative-maintenance (PM) plans — each with a frequency (every N days/weeks/months/quarters/bi-yearly/years), an ordered step checklist with per-step time estimates (the plan's estimated minutes auto-totals from the steps), and a components/parts list (part #, description, supplier, cost). Plans are attached to assets from the asset's **PM tab** with a per-asset interval (default set in Admin) and an end date pre-filled to the asset's depreciation end. Completing a service opens a structured form — performed-by (the logged-in user), a **required signature**, 1–5★ condition ratings (Safety/Is it safe?, Physical condition, Operating condition), dynamic completion notes, and photos — then checks off steps, captures parts cost, records history, and rolls the next-due date forward; due/overdue PM raises alerts. Completed forms (with signature, ratings, notes, and photos) are viewable from the **Maintenance** screen and from each asset's **PM tab**. Maintenance spend is tracked per asset in a dedicated **PM book** (Books screen → general ledger) that aggregates completion costs.

The **Parts inventory** screen (a sub-menu under Maintenance) is a standalone inventory of maintenance parts & supplies, kept separate from the depreciation register. Look up a part to see whether it's in stock, which **aisle/bin** to get it from, and how much is **reserved** vs available; add identification details like photos, measurements, manufacturer part #, and barcode. Stock can live in one or more **Work Location** contacts, and each part's **supplier** is chosen from the Contacts directory (vendors, service providers, or contractors). Stock movements (receive, issue, adjust, reserve, unreserve) are recorded with full history, and parts at/below their reorder point are flagged as low stock.

The **Contacts** screen is a directory of people and organizations — Employees, Vendors, Service Providers, Contractors, Work Locations, and Other — each with name, international-friendly address and phone, email, and notes, plus type-specific fields (employee ID/department/manager/start date; contractor tax ID or business license; vendor/service-provider star rating and credit term) and a per-contact notes log. It works fully without Workday; when Workday is connected, employee records are synced into Contacts with manager and start-date enrichment, and the sync can be **scheduled automatically** from the Admin → Workday panel.

The **Alerts** screen surfaces reminders for assets needing maintenance, overdue tasks, preventative-maintenance due dates, and warranties/leases about to expire. A background job (and an on-demand scan) computes alerts with a configurable lead time and severity, and — when SMTP is configured and alerts are enabled — emails a digest of new items via Nodemailer. SMTP/email server settings live in a dedicated panel in the Admin pane (the password is write-only and never returned by the API).

The Reports workbench is driven by a server report catalog covering depreciation (annual, monthly, quarterly, YTD, net book value, lifetime, projection), activity (rollforward, acquisitions, disposals & gain/loss, adjustments, construction-in-progress), journal entries (depreciation, disposals), tax (Form 4562, Form 4797, AMT, ACE, personal property tax, UBIA for §199A), a fixed-asset card, and a custom **Report Builder** with selectable columns. Every report can be saved with its options for reuse and exported to PDF, Excel, or CSV.

## Run

```bash
npm install
npm run dev
```

Open `http://localhost:5173`.

Default seeded login:

```text
admin@deprecore.local
ChangeMe123!
```

## Scripts

```bash
npm run dev      # API + Vite SPA
npm run build    # production SPA build
npm start        # Express API and built SPA
npm test         # API smoke test
```

## Configuration

Environment variables are shown in `.env.example`. The app also stores editable runtime settings in SQLite through the Admin screen, including server FQDN, allowed CORS domains, and database metadata.

| Variable | Default | Purpose |
| --- | --- | --- |
| `DEPRECORE_PORT` (or `PORT`) | `3000` | API/server port |
| `DEPRECORE_DATA_DIR` | `./data` | Data directory |
| `DEPRECORE_DB_PATH` | `./data/deprecore.sqlite` | SQLite database file |
| `DEPRECORE_SERVER_FQDN` | `localhost` | Server FQDN (also editable in Admin) |
| `DEPRECORE_CORS_ORIGINS` | `http://localhost:5173,...` | Allowed CORS origins (comma-separated) |
| `DEPRECORE_JWT_SECRET` | dev fallback | JWT signing secret — set a long random value in production |
| `DEPRECORE_ADMIN_PASSWORD` | `ChangeMe123!` | Password for the seeded admin user |

---

# API Reference

The API is served under `/api`. All payloads are JSON unless noted (file upload and export endpoints differ).

## Authentication

Authenticate with `POST /api/auth/login` to receive a JWT, then send it on every other request:

```
Authorization: Bearer <token>
```

Tokens expire after 12 hours. The only routes that do **not** require a token are `GET /api/health`, `GET /api/public/bootstrap`, and `POST /api/auth/login`.

```bash
# Login
curl -s http://localhost:3000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@deprecore.local","password":"ChangeMe123!"}'
# => { "token": "<jwt>", "user": { "id":1, "name":"...", "role":"admin", ... } }
```

## Roles

Four roles gate write access: `admin`, `finance`, `operations`, `viewer`. The capability matrix is returned by `GET /api/access-control`. Endpoints below list the roles allowed to call them; read endpoints are available to any authenticated user.

## Errors & status codes

Errors return `{ "error": "message" }` with an appropriate status:

| Status | Meaning |
| --- | --- |
| `400` | Validation error (bad body, invalid JSON, missing required field) |
| `401` | Missing/expired token or invalid credentials |
| `403` | Authenticated but role not permitted |
| `404` | Resource not found |
| `500` | Unhandled server error |

## Endpoints

### Auth & profile

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/health` | public | Liveness check |
| GET | `/api/public/bootstrap` | public | App name, setup status, DB driver |
| POST | `/api/auth/login` | public | Exchange credentials for a JWT |
| GET | `/api/profile` | any | Current user + preferences |
| PUT | `/api/profile` | any | Update preferences (e.g. theme) |

### Dashboard & reference data

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/dashboard` | any | Portfolio stats, charts, recent audit trail |
| GET | `/api/entities` | any | List entities/companies |
| POST | `/api/entities` | admin, finance | Create an entity |
| GET | `/api/references` | any | Reference values (locations, departments, categories) |

### Assets

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/assets` | any | List/filter assets (`?search=&status=&entity_id=&category=`) |
| POST | `/api/assets` | admin, finance, operations | Create asset (optionally with `books[]`) |
| GET | `/api/assets/lookup?code=` | any | Resolve by barcode value, asset ID, or serial number |
| GET | `/api/assets/:id` | any | Hydrated asset (books, files, warranties, tasks, notes, disposals, adjustments, leases) |
| PUT | `/api/assets/:id` | admin, finance, operations | Update asset (and `books[]` when provided) |
| DELETE | `/api/assets/:id` | admin, finance | Delete asset |
| POST | `/api/assets/mass-update` | admin, finance | Bulk field update `{ ids:[], changes:{} }` |
| GET | `/api/assets/:id/depreciation` | any | Multi-year schedule across active books (`?startYear=&endYear=`) |
| POST | `/api/assets/:id/files` | admin, finance, operations | Upload a file (multipart `file`) |
| GET | `/api/assets/:id/files/:fileId` | any | Download a stored file |
| DELETE | `/api/assets/:id/files/:fileId` | admin, finance, operations | Delete a file |
| POST | `/api/assets/:id/photos` | admin, finance, operations | Add an identification photo `{ data, name, mime, caption }` (base64 data URL) |
| PUT | `/api/assets/:id/photos/:photoId` | admin, finance, operations | Update a photo caption |
| DELETE | `/api/assets/:id/photos/:photoId` | admin, finance, operations | Delete a photo |
| POST | `/api/assets/:id/warranties` | admin, finance, operations | Add warranty/service agreement |
| DELETE | `/api/assets/:id/warranties/:warrantyId` | admin, finance, operations | Delete warranty |
| POST | `/api/assets/:id/tasks` | admin, finance, operations | Add task |
| PUT | `/api/assets/:id/tasks/:taskId` | admin, finance, operations | Update task (e.g. mark complete) |
| DELETE | `/api/assets/:id/tasks/:taskId` | admin, finance, operations | Delete task |
| POST | `/api/assets/:id/notes` | admin, finance, operations | Add timestamped note |
| DELETE | `/api/assets/:id/notes/:noteId` | admin, finance, operations | Delete note |

### Disposals & adjustments

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| POST | `/api/assets/:id/disposal/preview` | any | Compute gain/loss without committing |
| POST | `/api/assets/:id/disposals` | admin, finance | Record full/partial disposal |
| DELETE | `/api/assets/:id/disposals/:disposalId` | admin, finance | Reverse a disposal |
| POST | `/api/assets/:id/adjustments` | admin, finance | Record impairment/write-down/write-up |
| DELETE | `/api/assets/:id/adjustments/:adjustmentId` | admin, finance | Delete an adjustment |

Disposal body: `{ book_type, disposal_date, disposal_price, disposal_expense, method, property_type, partial, disposed_cost, notes }`. Adjustment body: `{ type, amount, book_type, adjustment_date, reason }` where `type` is `impairment | write_down | write_up | revaluation`.

### Leases

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/leases` | any | List leases (`?asset_id=`) |
| POST | `/api/leases` | admin, finance | Create lease |
| PUT | `/api/leases/:id` | admin, finance | Update lease |
| DELETE | `/api/leases/:id` | admin, finance | Delete lease |
| GET | `/api/leases/:id/schedule` | any | ASC 842-style amortization schedule |

Lease body: `{ asset_id, lessor, contract_value, start_date, end_date, discount_rate, payment_frequency }` where `payment_frequency` is `monthly | quarterly | semiannual | annual`.

### Books & general ledger

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/books` | any | Book definitions + available rule sets |
| POST | `/api/books` | admin, finance | Create a user-defined book `{ code, name, book_type, ... }` |
| PUT | `/api/books/:id` | admin, finance | Update book (name, type, enabled, `tax_rule_set_id`, defaults) |
| DELETE | `/api/books/:id` | admin, finance | Delete a book (blocked for primary/PM books or books in use) |
| GET | `/api/books/:code/ledger?year=` | any | GL rollup + per-asset detail for a book |
| POST | `/api/books/:code/ledger/export` | any | Export the GL (`{ year, format }`, format `pdf|xlsx|csv`) |

### Assignments & employees

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/employees` | any | List employees (`?search=&status=`) |
| POST | `/api/employees` | admin, finance, operations | Create/upsert employee |
| GET | `/api/assignments` | any | List assignments + summary (`?asset_id=&employee_id=&active=true`) |
| POST | `/api/assignments` | admin, finance, operations | Assign asset to employee/department/location |
| PUT | `/api/assignments/:id/release` | admin, finance, operations | Release an assignment |

### Contacts

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/contacts` | any | List contacts (`?type=&search=&status=`) |
| GET | `/api/contacts/:id` | any | Single contact with its notes log |
| POST | `/api/contacts` | admin, finance, operations | Create a contact |
| PUT | `/api/contacts/:id` | admin, finance, operations | Update a contact |
| DELETE | `/api/contacts/:id` | admin, finance | Delete a contact |
| POST | `/api/contacts/:id/notes` | admin, finance, operations | Add a note |
| DELETE | `/api/contacts/:id/notes/:noteId` | admin, finance, operations | Delete a note |

Contact `type` is `employee | vendor | service_provider | contractor | work_location | other`. Vendor/provider `credit_term` is `na | net_30 | net_60 | net_90 | net_180`; `rating` is 1–5. The Workday connector (`/api/workday/*`) upserts `employee` contacts; `PUT /api/workday/connection` accepts `sync_enabled` and `sync_interval_hours` for the automatic scheduled sync.

### Templates (data-entry forms)

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/templates` | any | List asset entry templates |
| POST | `/api/templates` | admin, finance | Create a template (defaults + custom fields) |

### Tax rule sets ("tax templates")

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/tax-rules` | any | List rule sets (parsed JSON) |
| POST | `/api/tax-rules` | admin, finance | Create a rule set |
| PUT | `/api/tax-rules/:id` | admin, finance | Update a rule set |
| DELETE | `/api/tax-rules/:id` | admin, finance | Delete a rule set |
| POST | `/api/tax-rules/:id/activate` | admin, finance | Activate (deactivates others in the same jurisdiction) |
| POST | `/api/tax-rules/expand` | admin, finance | Merge the generated 68-method catalog into a draft |

See [Tax Rule Sets](#tax-rule-sets-tax-templates-1) below for the full JSON schema.

### Reports

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/reports/catalog` | any | Report types + parameter specs |
| POST | `/api/reports/run` | any | Run a report `{ type, options }` → `{ report }` |
| POST | `/api/reports/export` | any | Export `{ type, options, format }` (`pdf|xlsx|csv`) |
| GET | `/api/saved-reports` | any | List saved reports |
| POST | `/api/saved-reports` | admin, finance, operations | Save a report `{ name, report_type, options }` |
| DELETE | `/api/saved-reports/:id` | admin, finance, operations | Delete a saved report |
| GET | `/api/reports/depreciation` | any | Legacy annual depreciation (`?year=&book=`) |
| GET | `/api/reports/monthly-depreciation` | any | Legacy monthly depreciation |
| GET | `/api/reports/depreciation.pdf` | any | Legacy PDF |

Report types include `depreciation_expense`, `monthly_depreciation`, `quarterly_depreciation`, `ytd_depreciation`, `net_book_value`, `lifetime_depreciation`, `projection`, `rollforward`, `acquisitions`, `disposals`, `adjustments`, `cip`, `journal_depreciation`, `journal_disposals`, `form_4562`, `form_4797`, `amt`, `ace`, `personal_property_tax`, `ubia`, `fixed_asset_card`, `custom`, and the maintenance set: `pm_due` (PM due before a selectable date), `pm_uncompleted` (assets with overdue PM), `pm_completed` (PM completed since a selectable date), `pm_plan` (printable individual PM plan via selector), and `alerts_since` (alerts raised since a selectable date). Parameter types include `select`, `multiselect`, `number`, and `date`. A report result has the shape `{ title, columns:[{key,label,format}], rows:[], totals:{}, chart?, meta? }`.

### Import / export

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/export/assets?format=` | any | Export assets (`json|csv|xlsx|xml`) |
| POST | `/api/import/assets` | admin, finance | Import assets (multipart `file`: JSON/CSV/XLSX/XML) |

### Preventative maintenance

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/pm-plans` | any | List PM plans (steps/components; photo `photo_count` only, no blobs) |
| GET | `/api/pm-plans/:id` | any | One plan with full guidance-photo image data |
| POST | `/api/pm-plans` | admin, finance, operations | Create a plan `{ name, frequency_value, frequency_unit, steps[], components[], photos[] }` (steps carry `est_minutes`; estimated minutes auto-total) |
| PUT | `/api/pm-plans/:id` | admin, finance, operations | Update a plan (photos: resend existing as `{ id, caption }`, new as `{ data, name, mime, caption }`) |
| DELETE | `/api/pm-plans/:id` | admin, finance | Delete a plan |
| GET | `/api/pm-settings` | any | PM defaults (frequency + alert lead days) |
| PUT | `/api/pm-settings` | admin | Save PM defaults |
| GET | `/api/assets/:id/pm` | any | Asset's attached PM schedules + completions |
| POST | `/api/assets/:id/pm` | admin, finance, operations | Attach a plan `{ plan_id, frequency_value, frequency_unit, start_date, end_date }` |
| PUT | `/api/assets/:id/pm/:apId` | admin, finance, operations | Update an asset's PM schedule |
| DELETE | `/api/assets/:id/pm/:apId` | admin, finance, operations | Detach a schedule |
| POST | `/api/assets/:id/pm/:apId/complete` | admin, finance, operations | Record a service `{ steps[], components[], cost, notes_list[], ratings{safety,physical,operating}, signature, photos[] }` (signature required) and advance next-due |
| GET | `/api/pm-completions` | any | List completed PM forms (`?asset_id=`) |
| GET | `/api/pm-completions/:id` | any | Full completion form (signature, ratings, notes, photos) |

`frequency_unit` is `days | weeks | months | quarters | semiannual | years`. PM schedules feed the alerts engine (`pm_due` / `pm_overdue`).

**Photos.** Assets carry **identification photos** (Photos tab in the asset workbench) — what it looks like, where it is, nameplate/serial labels — each with an optional caption. PM plans carry **guidance photos** (diagrams, part locations, before/after) that appear to the technician on the completion form. On a phone, the "Add photos" buttons open the camera (`capture`). Photos are stored as base64 data URLs; plan guidance photos are also surfaced on each asset's PM schedule (`/api/assets/:id/pm`).

### Parts inventory

A standalone inventory of maintenance parts & supplies (sub-menu under **Maintenance**). It is **not** part of the depreciation register — parts are not assets and do not depreciate. Each part tracks stock per location with **aisle/bin**, on-hand vs **reserved** quantities, a reorder point for low-stock flagging, identification details (measurements, manufacturer part #, barcode, photos), a **supplier** chosen from Contacts (vendors/service providers/contractors), and stock stored at one or more **Work Location** contacts.

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/parts` | any | List parts with aggregated stock (`?search=&category=&status=&low_stock=true`) |
| GET | `/api/parts/:id` | any | Single part with stock locations, photos, and movement history |
| POST | `/api/parts` | admin, finance, operations | Create a part `{ part_number, name, category, unit_of_measure, unit_cost, reorder_point, reorder_quantity, measurements, manufacturer, barcode, supplier_contact_id, status }` |
| PUT | `/api/parts/:id` | admin, finance, operations | Update a part |
| DELETE | `/api/parts/:id` | admin, finance | Delete a part |
| POST | `/api/parts/:id/stock` | admin, finance, operations | Create/update a stock location `{ id?, location_contact_id, aisle, bin, quantity, reserved }` |
| DELETE | `/api/parts/:id/stock/:stockId` | admin, finance, operations | Remove a stock location |
| POST | `/api/parts/:id/transactions` | admin, finance, operations | Record a movement `{ stock_id, type, quantity, asset_id?, notes? }` |
| POST | `/api/parts/:id/photos` | admin, finance, operations | Attach a photo `{ name, mime, data }` (base64 data URL) |
| DELETE | `/api/parts/:id/photos/:photoId` | admin, finance, operations | Delete a photo |

Movement `type` is `receive` (add on-hand), `issue` (consume), `adjust` (set on-hand to an absolute count), `reserve`, or `unreserve`. `low_stock` is flagged when on-hand ≤ a non-zero reorder point. `available` = on-hand − reserved. All stock/movement/photo endpoints return the fully hydrated part.

### Alerts & notifications

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/alerts` | any | List alerts (`?status=&type=`) + summary |
| POST | `/api/alerts/scan` | admin, finance, operations | Recompute alerts from due/expiring items |
| POST | `/api/alerts/run` | admin | Scan and notify (email digest and/or webhook) on new alerts |
| PUT | `/api/alerts/:id/acknowledge` | admin, finance, operations | Acknowledge an alert |
| PUT | `/api/alerts/:id/dismiss` | admin, finance, operations | Dismiss an alert (won't reappear) |
| POST | `/api/alerts/:id/ticket` | admin, finance, operations | Submit the alert as a ServiceNow incident (returns the existing one if already raised) |
| GET | `/api/notifications/settings` | admin | SMTP + alert + webhook settings (password/secret masked) |
| PUT | `/api/notifications/settings` | admin | Save settings (send `smtp_password`/`webhook_secret` only to change them; `webhook_secret_clear: true` removes the secret) |
| POST | `/api/notifications/test` | admin | Send a test email (`{ to }` optional) |
| POST | `/api/notifications/webhook-test` | admin | POST a test event to the configured webhook URL |

Alert types: `maintenance_overdue`, `maintenance_due`, `warranty_expired`, `warranty_expiring`, `lease_expiring`, `pm_due`, `pm_overdue`. Severity is `critical` (overdue/expired or within 7 days) or `warning`. The server also runs the scan-and-notify cycle ~twice daily; it's a no-op unless something is configured.

**Webhook delivery.** When `webhook_enabled` is true and `webhook_url` is set, each newly raised alert is delivered as its own JSON `POST` (one request per alert) on the alert cycle — independently of, or alongside, email. An alert is marked notified once any channel handles it, so it is delivered at most once. If `webhook_secret` is set, the raw request body is signed with HMAC-SHA256 and sent in the `X-DepreCore-Signature: sha256=<hex>` header so the receiver can verify authenticity. Per-alert payload:

```json
{
  "event": "alert",
  "id": 142, "type": "pm_overdue", "severity": "critical",
  "title": "PM overdue: Lift quarterly service",
  "message": "Lift quarterly service on FA-1042 was due 2026-05-20.",
  "due_date": "2026-05-20", "status": "open",
  "reference_type": "asset_pm", "reference_id": 88,
  "asset_id": 1042, "asset_code": "FA-1042",
  "created_at": "2026-06-04T12:00:00.000Z", "sent_at": "2026-06-04T12:05:00.000Z"
}
```

### Admin & integrations

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/api/settings` | admin | App settings |
| PUT | `/api/settings` | admin | Update settings `{ settings:{} }` |
| GET | `/api/users` | admin | List users |
| POST | `/api/users` | admin | Create user |
| PUT | `/api/users/:id` | admin | Update user (role/status/team/password) |
| GET | `/api/teams` | admin | List teams |
| POST | `/api/teams` | admin | Create team |
| GET | `/api/access-control` | admin | Roles + capability matrix |
| GET | `/api/workday/connection` | admin | Workday connector config |
| PUT | `/api/workday/connection` | admin | Save Workday connector |
| POST | `/api/workday/sync-workers` | admin | Pull workers from Workday |
| POST | `/api/workday/import-workers` | admin | Import a worker payload |
| GET | `/api/servicenow/settings` | admin | ServiceNow connection config (password masked) |
| PUT | `/api/servicenow/settings` | admin | Save ServiceNow config (send `servicenow_password` only to change it; `servicenow_password_clear: true` removes it) |
| POST | `/api/servicenow/test` | admin | Verify the connection (GETs the incident table) |
| POST | `/api/servicenow/cmdb-sync` | admin, finance | Pull configuration items from the CMDB into the asset register |

### ServiceNow integration

Configured in **Admin → ServiceNow integration** using **basic auth** over HTTPS against a ServiceNow instance (`https://<instance>.service-now.com`).

**Incident push.** Alerts can be submitted as ServiceNow incidents via the Table API (`POST /api/now/table/{incident_table}`). From the **Alerts** screen, the **ServiceNow** action on any open alert creates an incident and links it back (number + deep link shown in the Ticket column). Severity maps to impact/urgency (critical → 1/1, warning → 2/2, else 3/3); optional `assignment_group` and `caller_id` are applied when set. With **auto-create** enabled, the alert cycle raises an incident for each new *critical* alert automatically. Alerts store the linked `external_ticket_number` / `external_ticket_url` and won't duplicate.

**CMDB pull.** The asset register can be populated from the CMDB via the Table API (`GET /api/now/table/{ci_table}`, default `cmdb_ci_hardware`), requested with display values. A JSON **field map** (asset field → CI column, with sensible hardware defaults) controls population; an optional `sysparm_query` filters which CIs are pulled. Existing assets are matched by ServiceNow `sys_id`, then serial number, then asset tag, so re-syncing updates in place rather than duplicating. The CI's `sys_id` is stored on the asset (`servicenow_sys_id`).

---

# Tax Rule Sets ("tax templates")

A **tax rule set** is the JSON template that tells the depreciation engine which methods exist and how to compute them, plus the statutory limits for a jurisdiction (e.g. `US-FED`, `US-CA`). It satisfies the requirement for a JSON-driven system for federal/state/local depreciation-law updates.

Rule sets are stored in the `tax_rule_sets` table and edited in the **Tax rules** screen (Monaco JSON editor) or via the API. The starter federal template lives at `data/tax-rules/federal-2026.json` and is seeded on first run.

## How a rule set is selected

Each **book** (GAAP, Federal, …) has a `tax_rule_set_id`. When the engine depreciates an asset for a given book, it uses **that book's assigned rule set**; if a book has no assignment, it falls back to the globally *active* rule set. Assign rule sets to books on the **Books** screen.

> Because the engine looks up each asset book's `depreciation_method` **by code** inside the rule set's `methods[]` array, the assigned rule set must contain the method codes your assets use. Use **Regenerate method catalog** (UI) or `POST /api/tax-rules/expand` to inject the standard 68 methods, then add or edit any custom methods.

## Top-level fields

```jsonc
{
  "schema": "deprecore.taxRuleSet.v1",   // optional label
  "jurisdiction": "US-FED",                 // unique key with version
  "name": "Federal 68-method depreciation catalog",
  "version": "2026.2",                      // unique per jurisdiction
  "effectiveDate": "2026-01-01",            // ISO date
  "sourceNote": "Validate against current IRS guidance before filing.",
  "books": ["GAAP", "FEDERAL", "STATE", "AMT", "ACE", "USER"],
  "limits": { /* see below */ },
  "conventions": ["half_year","mid_quarter","mid_month","full_month","none"],
  "assetLives": [ { "code": "5Y", "label": "5-year property", "months": 60 } ],
  "methods": [ /* see below — the part the engine computes from */ ],
  "catalogNotes": ["free-form notes"]
}
```

| Field | Type | Notes |
| --- | --- | --- |
| `jurisdiction` | string | Paired with `version` as a unique key; assign to a book by id |
| `name`, `version`, `effectiveDate` | string | Metadata; `version` must be unique within a jurisdiction |
| `limits` | object | Statutory thresholds (reference/advisory — see below) |
| `conventions` | string[] | Conventions offered in the UI |
| `assetLives` | object[] | `{ code, label, months }` reference table of class lives |
| `methods` | object[] | **Required for calculation** — the method catalog (see below) |

## `limits` object

These values are stored for compliance reference and surfaced in the UI. The engine applies §179 and bonus **per asset-book** (from each book's `section_179_amount` and `bonus_percent`); the limits document the statutory caps your data entry should respect.

```jsonc
"limits": {
  "deMinimisSafeHarbor": {
    "defaultThreshold": 2500,
    "auditedFinancialStatementThreshold": 5000
  },
  "section179": {
    "deductionLimit": 2500000,
    "phaseOutBegins": 4000000,
    "cannotCreateLoss": true
  },
  "bonusDepreciation": {
    "qualifiedPropertyPercent": 100,
    "effectivePlacedInServiceAfter": "2025-01-19"
  }
}
```

## `methods[]` — the calculation catalog

Each method is referenced by an asset book's `depreciation_method` (its `code`). Shape:

| Field | Type | Used by | Notes |
| --- | --- | --- | --- |
| `code` | string | all | Unique identifier referenced by asset books |
| `family` | string | UI | Grouping label: `MACRS`, `ACRS`, `GAAP`, `Manual`, `Lease` |
| `label` | string | UI | Human-readable name |
| `formula` | string | engine | One of the formulas below — drives the math |
| `lifeMonths` | number | engine | Optional; overrides the book/asset useful life |
| `defaultConvention` | string | engine | Convention applied for first/last partial years |
| `rateMultiplier` | number | engine | Declining-balance rate (e.g. `2` = 200%, `1.5` = 150%) |
| `switchToStraightLine` | boolean | engine | MACRS-style switch to SL when it yields more |
| `rates` | number[] | engine | For `rate_table`: per-year fraction of basis |
| `fallbackFormula` | string | engine | Used when `rates` is empty (e.g. `straight_line`) |
| `system`, `legacy`, `sourceNote`, `requiresMonthTable`, `requiresListedPropertyTable` | mixed | reference | Informational only |

### Supported `formula` values

| `formula` | Computation | Honors fields |
| --- | --- | --- |
| `straight_line` | `basis / years`, prorated by convention | `lifeMonths`, `defaultConvention` |
| `sum_of_years_digits` | SYD acceleration | `lifeMonths` |
| `declining_balance` | Declining balance at `rateMultiplier`; optional SL switch | `rateMultiplier`, `switchToStraightLine`, `defaultConvention`, `lifeMonths` |
| `macrs_declining_balance` | Same as `declining_balance` (MACRS GDS/ADS) | same as above |
| `rate_table` | `basis × rates[year]`; falls back to `fallbackFormula` if `rates` empty | `rates`, `fallbackFormula`, `lifeMonths` |
| `acrs_alternate_straight_line` | Straight-line forced to half-year convention (legacy ACRS) | `lifeMonths` |
| `manual` | Reads per-year amounts from the asset book's `manual_depreciation` map | — |
| *(unknown)* | Falls back to `straight_line` | — |

### Depreciable basis & conventions

The engine computes, for each asset book:

```
depreciable basis = max(0, (cost − land − salvage) × businessUse%)  − §179 − bonus
§179  = min(basis, book.section_179_amount)
bonus = (basis − §179) × book.bonus_percent / 100      (taken in year 1)
```

First-year proration by convention (subsequent years run full, with the remainder pushed into the final year):

| Convention | First-year fraction |
| --- | --- |
| `half_year` | `0.5` |
| `mid_quarter` | Q1 `0.875`, Q2 `0.625`, Q3 `0.375`, Q4 `0.125` |
| `mid_month` | `(12 − month + 0.5) / 12` |
| `full_month` | `(13 − month) / 12` |
| `none` | `1` (full year) |

The effective life is `ceil(lifeMonths / 12)` where `lifeMonths` is the method's `lifeMonths`, else the book's `useful_life_months`, else the asset's, else 60.

## Worked examples

A minimal rule set with one custom straight-line method and one rate-table method:

```jsonc
{
  "jurisdiction": "US-CA",
  "name": "California starter",
  "version": "2026.1",
  "effectiveDate": "2026-01-01",
  "limits": { "section179": { "deductionLimit": 25000, "phaseOutBegins": 200000 } },
  "conventions": ["half_year", "mid_month", "none"],
  "assetLives": [{ "code": "5Y", "label": "5-year", "months": 60 }],
  "methods": [
    {
      "code": "ca_sl_5",
      "family": "GAAP",
      "label": "CA straight-line 5-year",
      "formula": "straight_line",
      "lifeMonths": 60,
      "defaultConvention": "half_year"
    },
    {
      "code": "ca_macrs_5",
      "family": "MACRS",
      "label": "CA MACRS 5-year (table)",
      "formula": "rate_table",
      "lifeMonths": 60,
      "defaultConvention": "half_year",
      "rates": [0.20, 0.32, 0.192, 0.1152, 0.1152, 0.0576]
    }
  ]
}
```

A 200% declining-balance MACRS method with the straight-line switch:

```jsonc
{
  "code": "macrs_gds_200db_5",
  "family": "MACRS",
  "label": "MACRS GDS 200% DB 5-year",
  "formula": "macrs_declining_balance",
  "lifeMonths": 60,
  "rateMultiplier": 2,
  "switchToStraightLine": true,
  "defaultConvention": "half_year"
}
```

## Creating a tax template

**In the UI (Tax rules screen, admin/finance):**

1. Click **New** (blank draft) or **Upload JSON** to load a file into the editor.
2. Fill in metadata (name, jurisdiction, version, effective date) and edit the JSON body in the Monaco editor — invalid JSON disables Save and shows the parse error.
3. Optionally click **Regenerate method catalog** to inject the standard 68 methods, then add/edit custom methods.
4. **Save**, then **Activate** to make it the active set for its jurisdiction. **Export** downloads the JSON; assign it to books on the **Books** screen.

**Via the API:**

```bash
TOKEN=... # from /api/auth/login

# (optional) expand a draft to include the 68-method catalog
curl -s http://localhost:3000/api/tax-rules/expand \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"rules_json":{"jurisdiction":"US-CA","version":"2026.1"}}'

# create
curl -s http://localhost:3000/api/tax-rules \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
        "jurisdiction":"US-CA","name":"California starter","version":"2026.1",
        "effective_date":"2026-01-01","active":false,
        "rules_json":{ "jurisdiction":"US-CA","version":"2026.1","methods":[ /* ... */ ] }
      }'

# activate (deactivates other US-CA sets)
curl -s -X POST http://localhost:3000/api/tax-rules/<id>/activate \
  -H "Authorization: Bearer $TOKEN"
```

Then assign the rule set to a book:

```bash
curl -s -X PUT http://localhost:3000/api/books/<bookId> \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"tax_rule_set_id": <ruleSetId>}'
```

---

# Asset entry templates

Separate from tax rule sets, **asset entry templates** (`/api/templates`) speed up data entry by pre-filling defaults and adding custom fields. A template has:

- `defaults` — an object of asset fields applied on selection (e.g. `category`, `useful_life_months`, `depreciation_method`, GL accounts).
- `custom_fields` — an array of field definitions: `{ key, label, type, required, defaultValue }` where `type` is `text | numeric | date | bool`.

```jsonc
{
  "name": "Laptop or workstation",
  "description": "Standard computer equipment, 5-year life",
  "defaults": { "category": "Computer Equipment", "useful_life_months": 60, "depreciation_method": "macrs_gds_200db_5" },
  "custom_fields": [
    { "key": "assigned_employee_id", "label": "Employee ID", "type": "text", "required": false },
    { "key": "calibration_due", "label": "Calibration due", "type": "date", "required": false }
  ]
}
```

---

## Compliance Note

The depreciation engine is built to support rule updates, projections, books, conventions, and method expansion. The included tax template is an engineering starter and should be validated against current IRS, state, and local guidance before production tax filing.
