diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f89e6ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +*.tsbuildinfo +.DS_Store +*.log +.env +.env.* +.gstack/ +apps/store-server/data/ +apps/hq/data/ +/data/ +# Real client data staging for the APEX->HQ import — never committed +import-drop/ diff --git a/apps/hq/schema.sql b/apps/hq/schema.sql new file mode 100644 index 0000000..eefbe86 --- /dev/null +++ b/apps/hq/schema.sql @@ -0,0 +1,115 @@ +-- SiMS HQ database DDL (generated snapshot 2026-07-10; source of truth: apps/hq/src/db.ts) +-- Engine today: SQLite. Production target: Postgres (D15) — types map 1:1 (TEXT/INTEGER). + +CREATE TABLE audit_log ( + id TEXT PRIMARY KEY, at_wall TEXT NOT NULL, user_id TEXT NOT NULL, action TEXT NOT NULL, + entity TEXT NOT NULL, entity_id TEXT NOT NULL, before_json TEXT, after_json TEXT +); + +CREATE TABLE client ( + id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, + gstin TEXT, state_code TEXT NOT NULL DEFAULT '32', address TEXT NOT NULL DEFAULT '', + contacts TEXT NOT NULL DEFAULT '[]', -- JSON [{name,phone,email,role}] + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('lead','active','dormant','lost')), + notes TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'hq', created_at TEXT NOT NULL +); + +CREATE TABLE client_module ( + id TEXT PRIMARY KEY, client_id TEXT NOT NULL, module_id TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'quoted' CHECK (status IN + ('quoted','ordered','installing','installed','trained','live','expired','cancelled')), + kind TEXT NOT NULL, edition TEXT NOT NULL DEFAULT 'standard', + installed_on TEXT, completed_on TEXT, trained_on TEXT, + next_renewal TEXT, active INTEGER NOT NULL DEFAULT 1 +); + +CREATE TABLE doc_series ( + doc_type TEXT NOT NULL, fy TEXT NOT NULL, prefix TEXT NOT NULL, next_seq INTEGER NOT NULL, + PRIMARY KEY (doc_type, fy) +); + +CREATE TABLE document ( + id TEXT PRIMARY KEY, doc_type TEXT NOT NULL CHECK (doc_type IN + ('QUOTATION','PROFORMA','INVOICE','RECEIPT','CREDIT_NOTE')), + doc_no TEXT, fy TEXT NOT NULL, client_id TEXT NOT NULL, doc_date TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN + ('draft','sent','accepted','invoiced','part_paid','paid','lost','cancelled')), + ref_doc_id TEXT, -- QT→PI→INV chain; CN → the invoice it amends + taxable_paise INTEGER NOT NULL, cgst_paise INTEGER NOT NULL, sgst_paise INTEGER NOT NULL, + igst_paise INTEGER NOT NULL, round_off_paise INTEGER NOT NULL, payable_paise INTEGER NOT NULL, + payload TEXT NOT NULL, -- JSON { lines: BillLine[], totals: BillTotals, terms?: string } + source TEXT NOT NULL DEFAULT 'hq', -- 'apex' rows keep legacy numbers, exempt from series + created_by TEXT NOT NULL, created_at TEXT NOT NULL, + UNIQUE (doc_no) +); + +CREATE TABLE document_event ( + id TEXT PRIMARY KEY, document_id TEXT NOT NULL, at_wall TEXT NOT NULL, + kind TEXT NOT NULL, meta TEXT NOT NULL DEFAULT '{}' +); + +CREATE TABLE email_account ( + id TEXT PRIMARY KEY, address TEXT NOT NULL, refresh_token_enc TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','dead')), updated_at TEXT NOT NULL +); + +CREATE TABLE email_log ( + id TEXT PRIMARY KEY, document_id TEXT, to_addr TEXT NOT NULL, subject TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('sent','failed')), gmail_message_id TEXT, + error TEXT, at_wall TEXT NOT NULL +); + +CREATE TABLE module ( + id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, + sac TEXT NOT NULL DEFAULT '998313', -- IT services default; CA session confirms per module + allowed_kinds TEXT NOT NULL DEFAULT '["one_time","monthly","yearly","usage"]', + multi_subscription INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1, + quote_content TEXT NOT NULL DEFAULT '[]' -- JSON string[]: "what's included" lines on quotes +); + +CREATE TABLE module_price_book ( + id TEXT PRIMARY KEY, module_id TEXT NOT NULL, edition TEXT NOT NULL DEFAULT 'standard', + kind TEXT NOT NULL, price_paise INTEGER NOT NULL, effective_from TEXT NOT NULL +); + +CREATE TABLE payment ( + id TEXT PRIMARY KEY, client_id TEXT NOT NULL, received_on TEXT NOT NULL, + mode TEXT NOT NULL CHECK (mode IN ('bank','upi','cheque','cash','other')), + reference TEXT NOT NULL DEFAULT '', amount_paise INTEGER NOT NULL, + tds_paise INTEGER NOT NULL DEFAULT 0, created_by TEXT NOT NULL, created_at TEXT NOT NULL +); + +CREATE TABLE payment_allocation ( + id TEXT PRIMARY KEY, payment_id TEXT NOT NULL, document_id TEXT NOT NULL, + amount_paise INTEGER NOT NULL -- includes this allocation's TDS share +); + +CREATE TABLE session ( + token TEXT PRIMARY KEY, staff_id TEXT NOT NULL, expires_at TEXT NOT NULL +); + +CREATE TABLE setting ( + key TEXT PRIMARY KEY, value TEXT NOT NULL +); + +CREATE TABLE staff_user ( + id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('owner','staff')), + pw_salt TEXT NOT NULL, pw_hash TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 +); + +CREATE TABLE stg_client ( + row_no INTEGER PRIMARY KEY, code TEXT, name TEXT, gstin TEXT, state_code TEXT, + address TEXT, phone TEXT, email TEXT, status TEXT, problems TEXT NOT NULL DEFAULT '[]' +); + +CREATE TABLE stg_invoice ( + row_no INTEGER PRIMARY KEY, client_code TEXT, doc_no TEXT, doc_date TEXT, + taxable_paise INTEGER, tax_paise INTEGER, total_paise INTEGER, paid INTEGER, + problems TEXT NOT NULL DEFAULT '[]' +); + +CREATE TABLE tax_class ( + class_code TEXT NOT NULL, rate_pct_bp INTEGER NOT NULL, + cess_pct_bp INTEGER NOT NULL DEFAULT 0, effective_from TEXT NOT NULL, effective_to TEXT +); diff --git a/docs/06-DECISIONS.md b/docs/06-DECISIONS.md index c4adc54..55d541c 100644 --- a/docs/06-DECISIONS.md +++ b/docs/06-DECISIONS.md @@ -139,6 +139,8 @@ AWS attribution, no path to the fleet console). packages are shared. Guardrails: a bundle-isolation check in the test suite (store bundle contains no HQ code and vice versa), per-app version tags, and the rule that **HQ's DB is the cloud tier's nucleus** — the tenant registry is born from HQ's client - table (same UUIDs, promoted once), never built beside it. Engine may swap to Postgres - at AWS deploy time (portable repos make it cheap). Physical repo split stays a cheap, + table (same UUIDs, promoted once), never built beside it. **Postgres locked as the + production engine (founder, 2026-07-10)** — swap runs as its own task after HQ-2, + before the real-data import, so live data never migrates engines; SQLite stays the + dev/test engine behind the same repositories. Physical repo split stays a cheap, reversible option if practice ever demands it. diff --git a/docs/14-SPEC-HQ-CONSOLE.md b/docs/14-SPEC-HQ-CONSOLE.md index c245006..80aeae4 100644 --- a/docs/14-SPEC-HQ-CONSOLE.md +++ b/docs/14-SPEC-HQ-CONSOLE.md @@ -25,7 +25,7 @@ tracking against our catalog, no AWS attribution, no path to the fleet console). |---|---| | App | `apps/hq` — Node server + web app + JSON API, same pattern as `apps/store-server` | | Hosting | Small AWS instance (Mumbai), HTTPS; store product stays local per D14 — this is vendor-side cloud, which doc 11 already prescribes | -| DB | SQLite (WAL) behind portable repositories (`repos.ts` pattern); nightly + on-change backup to S3; restore drill scheduled. **This database is the nucleus of the future cloud tier** — at HQ-4 the tenant registry grows *from* it (same rows, same UUIDs), never beside it; there is never a second client list to reconcile. Consider swapping the engine to Postgres at AWS deploy time (cheap behind the repos; removes even the engine migration later) | +| DB | SQLite (WAL) behind portable repositories (`repos.ts` pattern); nightly + on-change backup to S3; restore drill scheduled. **This database is the nucleus of the future cloud tier** — at HQ-4 the tenant registry grows *from* it (same rows, same UUIDs), never beside it; there is never a second client list to reconcile. **Postgres is the locked production engine** (founder call 2026-07-10): the swap is a dedicated task sequenced AFTER HQ-2 and BEFORE the real-data import and go-live, so the 300 clients land once, directly in the final engine; SQLite remains the dev/test engine behind the same repositories. DDL snapshot for mapping work: `apps/hq/schema.sql` | | Reuse | `@sims/billing-engine` **compute engine** (our invoices are GST documents — computed to the paisa), `@sims/domain` money/FY/UUIDv7 helpers + doc series (**generalized in HQ-1**: scope becomes an opaque key, separator/width configurable, 16-char GST rule kept — today it is per-counter), `@sims/ui` (design system), `@sims/auth` patterns (scrypt, lockout). HQ defines its **own document/payment types** — quotation/proforma/credit-note and B2B payment modes (NEFT/cheque) don't exist in the retail document model; shared packages stay generic (D15) | | Auth | Email + password (scrypt), optional TOTP for owner; roles: **Owner** (everything incl. pricing/finance), **Staff** (operations; finance edits owner-gated). Small team — same people do sales, accounts, support | | Audit | Every action logged — same discipline as the store product |