@hasna/invoices 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +109 -62
  3. package/dashboard/dist/assets/index-BhR1BMmj.js +139 -0
  4. package/dashboard/dist/assets/index-khzrvnCp.css +1 -0
  5. package/dashboard/dist/index.html +13 -0
  6. package/dashboard/dist/logo.jpg +0 -0
  7. package/dist/cli/index.js +10111 -9633
  8. package/dist/cli/tui.d.ts +2 -0
  9. package/dist/cli/tui.d.ts.map +1 -0
  10. package/dist/db/agents.d.ts +19 -0
  11. package/dist/db/agents.d.ts.map +1 -0
  12. package/dist/db/cloud.d.ts +20 -0
  13. package/dist/db/cloud.d.ts.map +1 -0
  14. package/dist/db/core.test.d.ts +2 -0
  15. package/dist/db/core.test.d.ts.map +1 -0
  16. package/dist/db/database.d.ts +7 -8
  17. package/dist/db/database.d.ts.map +1 -1
  18. package/dist/db/invoices.d.ts +77 -90
  19. package/dist/db/invoices.d.ts.map +1 -1
  20. package/dist/db/migrate.d.ts +4 -0
  21. package/dist/db/migrate.d.ts.map +1 -0
  22. package/dist/db/pg.d.ts +3 -0
  23. package/dist/db/pg.d.ts.map +1 -0
  24. package/dist/db/schema.d.ts +6 -0
  25. package/dist/db/schema.d.ts.map +1 -0
  26. package/dist/index.d.ts +8 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1061 -510
  29. package/dist/lib/version.d.ts +2 -0
  30. package/dist/lib/version.d.ts.map +1 -0
  31. package/dist/mcp/index.d.ts +1 -1
  32. package/dist/mcp/index.js +1320 -766
  33. package/dist/server/index.d.ts +3 -0
  34. package/dist/server/index.d.ts.map +1 -0
  35. package/dist/server/index.js +14919 -0
  36. package/dist/types/index.d.ts +18 -0
  37. package/dist/types/index.d.ts.map +1 -0
  38. package/package.json +21 -14
  39. package/dist/db/business.d.ts +0 -93
  40. package/dist/db/business.d.ts.map +0 -1
  41. package/dist/db/clients.d.ts +0 -45
  42. package/dist/db/clients.d.ts.map +0 -1
  43. package/dist/db/migrations.d.ts +0 -7
  44. package/dist/db/migrations.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  // @bun
2
+ // src/lib/version.ts
3
+ var VERSION = "0.0.1";
4
+ // src/db/database.ts
5
+ import { mkdirSync as mkdirSync3 } from "fs";
6
+ import { dirname as dirname2, join as join5 } from "path";
7
+
2
8
  // node_modules/@hasna/cloud/dist/index.js
3
9
  import { createRequire } from "module";
4
10
  import { Database } from "bun:sqlite";
@@ -16,6 +22,7 @@ import { join as join2 } from "path";
16
22
  import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
17
23
  import { join as join3 } from "path";
18
24
  import { homedir as homedir3 } from "os";
25
+ import { hostname } from "os";
19
26
  import { homedir as homedir4 } from "os";
20
27
  import { join as join4 } from "path";
21
28
  import { join as join6, dirname } from "path";
@@ -1359,7 +1366,7 @@ var require_cert_signatures = __commonJS((exports, module) => {
1359
1366
  module.exports = { signatureAlgorithmHashFromCertificate };
1360
1367
  });
1361
1368
  var require_sasl = __commonJS((exports, module) => {
1362
- var crypto2 = require_utils2();
1369
+ var crypto = require_utils2();
1363
1370
  var { signatureAlgorithmHashFromCertificate } = require_cert_signatures();
1364
1371
  function startSession(mechanisms, stream) {
1365
1372
  const candidates = ["SCRAM-SHA-256"];
@@ -1372,7 +1379,7 @@ var require_sasl = __commonJS((exports, module) => {
1372
1379
  if (mechanism === "SCRAM-SHA-256-PLUS" && typeof stream.getPeerCertificate !== "function") {
1373
1380
  throw new Error("SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate");
1374
1381
  }
1375
- const clientNonce = crypto2.randomBytes(18).toString("base64");
1382
+ const clientNonce = crypto.randomBytes(18).toString("base64");
1376
1383
  const gs2Header = mechanism === "SCRAM-SHA-256-PLUS" ? "p=tls-server-end-point" : stream ? "y" : "n";
1377
1384
  return {
1378
1385
  mechanism,
@@ -1408,20 +1415,20 @@ var require_sasl = __commonJS((exports, module) => {
1408
1415
  let hashName = signatureAlgorithmHashFromCertificate(peerCert);
1409
1416
  if (hashName === "MD5" || hashName === "SHA-1")
1410
1417
  hashName = "SHA-256";
1411
- const certHash = await crypto2.hashByName(hashName, peerCert);
1418
+ const certHash = await crypto.hashByName(hashName, peerCert);
1412
1419
  const bindingData = Buffer.concat([Buffer.from("p=tls-server-end-point,,"), Buffer.from(certHash)]);
1413
1420
  channelBinding = bindingData.toString("base64");
1414
1421
  }
1415
1422
  const clientFinalMessageWithoutProof = "c=" + channelBinding + ",r=" + sv.nonce;
1416
1423
  const authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
1417
1424
  const saltBytes = Buffer.from(sv.salt, "base64");
1418
- const saltedPassword = await crypto2.deriveKey(password, saltBytes, sv.iteration);
1419
- const clientKey = await crypto2.hmacSha256(saltedPassword, "Client Key");
1420
- const storedKey = await crypto2.sha256(clientKey);
1421
- const clientSignature = await crypto2.hmacSha256(storedKey, authMessage);
1425
+ const saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration);
1426
+ const clientKey = await crypto.hmacSha256(saltedPassword, "Client Key");
1427
+ const storedKey = await crypto.sha256(clientKey);
1428
+ const clientSignature = await crypto.hmacSha256(storedKey, authMessage);
1422
1429
  const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString("base64");
1423
- const serverKey = await crypto2.hmacSha256(saltedPassword, "Server Key");
1424
- const serverSignatureBytes = await crypto2.hmacSha256(serverKey, authMessage);
1430
+ const serverKey = await crypto.hmacSha256(saltedPassword, "Server Key");
1431
+ const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage);
1425
1432
  session.message = "SASLResponse";
1426
1433
  session.serverSignature = Buffer.from(serverSignatureBytes).toString("base64");
1427
1434
  session.response = clientFinalMessageWithoutProof + ",p=" + clientProof;
@@ -1584,11 +1591,11 @@ var require_pg_connection_string = __commonJS((exports, module) => {
1584
1591
  config.client_encoding = result.searchParams.get("encoding");
1585
1592
  return config;
1586
1593
  }
1587
- const hostname = dummyHost ? "" : result.hostname;
1594
+ const hostname2 = dummyHost ? "" : result.hostname;
1588
1595
  if (!config.host) {
1589
- config.host = decodeURIComponent(hostname);
1590
- } else if (hostname && /^%2f/i.test(hostname)) {
1591
- result.pathname = hostname + result.pathname;
1596
+ config.host = decodeURIComponent(hostname2);
1597
+ } else if (hostname2 && /^%2f/i.test(hostname2)) {
1598
+ result.pathname = hostname2 + result.pathname;
1592
1599
  }
1593
1600
  if (!config.port) {
1594
1601
  config.port = result.port;
@@ -3439,7 +3446,7 @@ var require_client = __commonJS((exports, module) => {
3439
3446
  var Query = require_query();
3440
3447
  var defaults = require_defaults();
3441
3448
  var Connection = require_connection();
3442
- var crypto2 = require_utils2();
3449
+ var crypto = require_utils2();
3443
3450
  var activeQueryDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.activeQuery is deprecated and will be removed in pg@9.0");
3444
3451
  var queryQueueDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.queryQueue is deprecated and will be removed in pg@9.0.");
3445
3452
  var pgPassDeprecationNotice = nodeUtils.deprecate(() => {}, "pgpass support is deprecated and will be removed in pg@9.0. " + "You can provide an async function as the password property to the Client/Pool constructor that returns a password instead. Within this function you can call the pgpass module in your own code.");
@@ -3655,7 +3662,7 @@ var require_client = __commonJS((exports, module) => {
3655
3662
  _handleAuthMD5Password(msg) {
3656
3663
  this._getPassword(async () => {
3657
3664
  try {
3658
- const hashedPassword = await crypto2.postgresMd5PasswordHash(this.user, this.password, msg.salt);
3665
+ const hashedPassword = await crypto.postgresMd5PasswordHash(this.user, this.password, msg.salt);
3659
3666
  this.connection.password(hashedPassword);
3660
3667
  } catch (e) {
3661
3668
  this.emit("error", e);
@@ -4933,6 +4940,18 @@ function sqliteToPostgres(sql) {
4933
4940
  }
4934
4941
  return out;
4935
4942
  }
4943
+ function translateDdl(ddl, dialect) {
4944
+ if (dialect === "sqlite")
4945
+ return ddl;
4946
+ let out = ddl;
4947
+ out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
4948
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "");
4949
+ out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
4950
+ out = out.replace(/\bBLOB\b/gi, "BYTEA");
4951
+ out = sqliteToPostgres(out);
4952
+ return out;
4953
+ }
4954
+
4936
4955
  class SqliteAdapter {
4937
4956
  db;
4938
4957
  constructor(path) {
@@ -5120,6 +5139,63 @@ class PgAdapter {
5120
5139
  return this.pool;
5121
5140
  }
5122
5141
  }
5142
+
5143
+ class PgAdapterAsync {
5144
+ pool;
5145
+ constructor(arg) {
5146
+ if (typeof arg === "string") {
5147
+ const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
5148
+ this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
5149
+ } else {
5150
+ this.pool = arg;
5151
+ }
5152
+ }
5153
+ async run(sql, ...params) {
5154
+ const pgSql = translateSql(sql, "pg");
5155
+ const pgParams = translateParams(params);
5156
+ const res = await this.pool.query(pgSql, pgParams);
5157
+ return {
5158
+ changes: res.rowCount ?? 0,
5159
+ lastInsertRowid: res.rows?.[0]?.id ?? 0
5160
+ };
5161
+ }
5162
+ async get(sql, ...params) {
5163
+ const pgSql = translateSql(sql, "pg");
5164
+ const pgParams = translateParams(params);
5165
+ const res = await this.pool.query(pgSql, pgParams);
5166
+ return res.rows[0] ?? null;
5167
+ }
5168
+ async all(sql, ...params) {
5169
+ const pgSql = translateSql(sql, "pg");
5170
+ const pgParams = translateParams(params);
5171
+ const res = await this.pool.query(pgSql, pgParams);
5172
+ return res.rows;
5173
+ }
5174
+ async exec(sql) {
5175
+ const pgSql = translateSql(sql, "pg");
5176
+ await this.pool.query(pgSql);
5177
+ }
5178
+ async close() {
5179
+ await this.pool.end();
5180
+ }
5181
+ async transaction(fn) {
5182
+ const client = await this.pool.connect();
5183
+ try {
5184
+ await client.query("BEGIN");
5185
+ const result = await fn(client);
5186
+ await client.query("COMMIT");
5187
+ return result;
5188
+ } catch (err) {
5189
+ await client.query("ROLLBACK");
5190
+ throw err;
5191
+ } finally {
5192
+ client.release();
5193
+ }
5194
+ }
5195
+ get raw() {
5196
+ return this.pool;
5197
+ }
5198
+ }
5123
5199
  var init_adapter = __esm(() => {
5124
5200
  init_esm();
5125
5201
  });
@@ -9382,7 +9458,494 @@ var init_discover = __esm(() => {
9382
9458
  });
9383
9459
  init_adapter();
9384
9460
  init_config();
9461
+ async function syncPush(local, remote, options) {
9462
+ const orderedTables = await getTableOrder(remote, options.tables);
9463
+ return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
9464
+ }
9465
+ async function syncPull(remote, local, options) {
9466
+ const orderedTables = await getTableOrder(remote, options.tables);
9467
+ return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
9468
+ }
9469
+ async function getTableOrder(remote, tables) {
9470
+ if (tables.length <= 1)
9471
+ return tables;
9472
+ try {
9473
+ const fks = await remote.all(`
9474
+ SELECT DISTINCT
9475
+ tc.table_name AS source_table,
9476
+ ccu.table_name AS referenced_table
9477
+ FROM information_schema.table_constraints tc
9478
+ JOIN information_schema.constraint_column_usage ccu
9479
+ ON tc.constraint_name = ccu.constraint_name
9480
+ AND tc.table_schema = ccu.table_schema
9481
+ WHERE tc.constraint_type = 'FOREIGN KEY'
9482
+ AND tc.table_schema = 'public'
9483
+ `);
9484
+ if (fks.length > 0) {
9485
+ return topoSort(tables, fks);
9486
+ }
9487
+ } catch {}
9488
+ return heuristicOrder(tables);
9489
+ }
9490
+ function topoSort(tables, fks) {
9491
+ const tableSet = new Set(tables);
9492
+ const deps = new Map;
9493
+ for (const t of tables) {
9494
+ deps.set(t, new Set);
9495
+ }
9496
+ for (const fk of fks) {
9497
+ if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
9498
+ deps.get(fk.source_table).add(fk.referenced_table);
9499
+ }
9500
+ }
9501
+ const sorted = [];
9502
+ const visited = new Set;
9503
+ const visiting = new Set;
9504
+ function visit(table) {
9505
+ if (visited.has(table))
9506
+ return;
9507
+ if (visiting.has(table)) {
9508
+ sorted.push(table);
9509
+ visited.add(table);
9510
+ return;
9511
+ }
9512
+ visiting.add(table);
9513
+ const tableDeps = deps.get(table) ?? new Set;
9514
+ for (const dep of tableDeps) {
9515
+ visit(dep);
9516
+ }
9517
+ visiting.delete(table);
9518
+ visited.add(table);
9519
+ sorted.push(table);
9520
+ }
9521
+ for (const t of tables) {
9522
+ visit(t);
9523
+ }
9524
+ return sorted;
9525
+ }
9526
+ function heuristicOrder(tables) {
9527
+ const sorted = [...tables].sort((a, b) => {
9528
+ const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
9529
+ const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
9530
+ if (aIsChild && !bIsChild)
9531
+ return 1;
9532
+ if (!aIsChild && bIsChild)
9533
+ return -1;
9534
+ return a.localeCompare(b);
9535
+ });
9536
+ return sorted;
9537
+ }
9538
+ function getSqlitePrimaryKeys(adapter, table) {
9539
+ try {
9540
+ const cols = adapter.all(`PRAGMA table_info("${table}")`);
9541
+ const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
9542
+ return pkCols;
9543
+ } catch {
9544
+ return [];
9545
+ }
9546
+ }
9547
+ async function getPgPrimaryKeys(adapter, table) {
9548
+ try {
9549
+ const rows = await adapter.all(`
9550
+ SELECT kcu.column_name, kcu.ordinal_position
9551
+ FROM information_schema.table_constraints tc
9552
+ JOIN information_schema.key_column_usage kcu
9553
+ ON tc.constraint_name = kcu.constraint_name
9554
+ AND tc.table_schema = kcu.table_schema
9555
+ WHERE tc.constraint_type = 'PRIMARY KEY'
9556
+ AND tc.table_schema = 'public'
9557
+ AND tc.table_name = '${table}'
9558
+ ORDER BY kcu.ordinal_position
9559
+ `);
9560
+ return rows.map((r) => r.column_name);
9561
+ } catch {
9562
+ return [];
9563
+ }
9564
+ }
9565
+ async function detectPrimaryKeys(adapter, table) {
9566
+ if (isAsyncAdapter(adapter)) {
9567
+ return getPgPrimaryKeys(adapter, table);
9568
+ }
9569
+ return getSqlitePrimaryKeys(adapter, table);
9570
+ }
9571
+ async function resolvePrimaryKeys(source, target, table, pkOption) {
9572
+ if (pkOption) {
9573
+ return Array.isArray(pkOption) ? pkOption : [pkOption];
9574
+ }
9575
+ let pks = await detectPrimaryKeys(source, table);
9576
+ if (pks.length === 0) {
9577
+ pks = await detectPrimaryKeys(target, table);
9578
+ }
9579
+ return pks;
9580
+ }
9581
+ function pgTypeToSqlite(pgType) {
9582
+ const t = pgType.toLowerCase();
9583
+ if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
9584
+ return "INTEGER";
9585
+ if (t.includes("bool"))
9586
+ return "INTEGER";
9587
+ if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
9588
+ return "REAL";
9589
+ if (t === "bytea")
9590
+ return "BLOB";
9591
+ return "TEXT";
9592
+ }
9593
+ async function ensureTableInSqliteFromPg(target, source, table) {
9594
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
9595
+ if (existing.length > 0)
9596
+ return false;
9597
+ const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
9598
+ FROM information_schema.columns
9599
+ WHERE table_schema = 'public' AND table_name = '${table}'
9600
+ ORDER BY ordinal_position`);
9601
+ if (cols.length === 0)
9602
+ return false;
9603
+ const pkCols = await source.all(`SELECT kcu.column_name
9604
+ FROM information_schema.table_constraints tc
9605
+ JOIN information_schema.key_column_usage kcu
9606
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
9607
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
9608
+ ORDER BY kcu.ordinal_position`);
9609
+ const pkSet = new Set(pkCols.map((c) => c.column_name));
9610
+ const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
9611
+ const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
9612
+ const colDefs = filteredCols.map((c) => {
9613
+ const sqliteType = pgTypeToSqlite(c.data_type);
9614
+ const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
9615
+ return `"${c.column_name}" ${sqliteType}${notNull}`;
9616
+ });
9617
+ if (pkSet.size > 0) {
9618
+ const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
9619
+ colDefs.push(`PRIMARY KEY (${pkList})`);
9620
+ }
9621
+ const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
9622
+ target.exec(sql);
9623
+ process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
9624
+ `);
9625
+ return true;
9626
+ }
9627
+ async function ensureTablesExist(source, target, tables) {
9628
+ for (const table of tables) {
9629
+ if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
9630
+ await ensureTableInSqliteFromPg(target, source, table);
9631
+ }
9632
+ }
9633
+ }
9634
+ async function filterColumnsForTarget(target, table, sourceColumns) {
9635
+ try {
9636
+ if (!isAsyncAdapter(target)) {
9637
+ const colInfo = target.all(`PRAGMA table_info("${table}")`);
9638
+ if (Array.isArray(colInfo) && colInfo.length > 0) {
9639
+ const targetCols = new Set(colInfo.map((c) => c.name));
9640
+ const filtered = sourceColumns.filter((c) => targetCols.has(c));
9641
+ if (filtered.length < sourceColumns.length) {
9642
+ const dropped = sourceColumns.filter((c) => !targetCols.has(c));
9643
+ process.stderr.write(` [sync] ${table}: dropping ${dropped.length} columns not in target: ${dropped.join(", ")}
9644
+ `);
9645
+ }
9646
+ return filtered;
9647
+ }
9648
+ } else {
9649
+ const colInfo = await target.all(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${table}'`);
9650
+ if (colInfo.length > 0) {
9651
+ const targetCols = new Set(colInfo.map((c) => c.column_name));
9652
+ return sourceColumns.filter((c) => targetCols.has(c));
9653
+ }
9654
+ }
9655
+ } catch {}
9656
+ return sourceColumns;
9657
+ }
9658
+ async function syncTransfer(source, target, options, _direction) {
9659
+ const {
9660
+ tables,
9661
+ onProgress,
9662
+ batchSize = 100,
9663
+ conflictColumn = "updated_at",
9664
+ primaryKey: pkOption
9665
+ } = options;
9666
+ const results = [];
9667
+ const sqliteTarget = !isAsyncAdapter(target) ? target : null;
9668
+ await ensureTablesExist(source, target, tables);
9669
+ if (sqliteTarget) {
9670
+ try {
9671
+ sqliteTarget.exec("PRAGMA foreign_keys = OFF");
9672
+ } catch {}
9673
+ }
9674
+ try {
9675
+ for (let i = 0;i < tables.length; i++) {
9676
+ const table = tables[i];
9677
+ const result = {
9678
+ table,
9679
+ rowsRead: 0,
9680
+ rowsWritten: 0,
9681
+ rowsSkipped: 0,
9682
+ errors: []
9683
+ };
9684
+ try {
9685
+ onProgress?.({
9686
+ table,
9687
+ phase: "reading",
9688
+ rowsRead: 0,
9689
+ rowsWritten: 0,
9690
+ totalTables: tables.length,
9691
+ currentTableIndex: i
9692
+ });
9693
+ const rows = await readAll(source, `SELECT * FROM "${table}"`);
9694
+ result.rowsRead = rows.length;
9695
+ if (rows.length === 0) {
9696
+ onProgress?.({
9697
+ table,
9698
+ phase: "done",
9699
+ rowsRead: 0,
9700
+ rowsWritten: 0,
9701
+ totalTables: tables.length,
9702
+ currentTableIndex: i
9703
+ });
9704
+ results.push(result);
9705
+ continue;
9706
+ }
9707
+ const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
9708
+ const sourceColumns = Object.keys(rows[0]);
9709
+ const columns = await filterColumnsForTarget(target, table, sourceColumns);
9710
+ if (pkColumns.length === 0) {
9711
+ result.errors.push(`Table "${table}" has no primary key \u2014 inserting without conflict handling`);
9712
+ onProgress?.({
9713
+ table,
9714
+ phase: "writing",
9715
+ rowsRead: result.rowsRead,
9716
+ rowsWritten: 0,
9717
+ totalTables: tables.length,
9718
+ currentTableIndex: i
9719
+ });
9720
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
9721
+ const batch = rows.slice(offset, offset + batchSize);
9722
+ try {
9723
+ if (isAsyncAdapter(target)) {
9724
+ await batchInsertPg(target, table, columns, batch);
9725
+ } else {
9726
+ batchInsertSqlite(target, table, columns, batch);
9727
+ }
9728
+ result.rowsWritten += batch.length;
9729
+ } catch (err) {
9730
+ result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
9731
+ }
9732
+ }
9733
+ onProgress?.({
9734
+ table,
9735
+ phase: "done",
9736
+ rowsRead: result.rowsRead,
9737
+ rowsWritten: result.rowsWritten,
9738
+ totalTables: tables.length,
9739
+ currentTableIndex: i
9740
+ });
9741
+ results.push(result);
9742
+ continue;
9743
+ }
9744
+ const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
9745
+ if (missingPks.length > 0) {
9746
+ result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
9747
+ results.push(result);
9748
+ continue;
9749
+ }
9750
+ onProgress?.({
9751
+ table,
9752
+ phase: "writing",
9753
+ rowsRead: result.rowsRead,
9754
+ rowsWritten: 0,
9755
+ totalTables: tables.length,
9756
+ currentTableIndex: i
9757
+ });
9758
+ const updateCols = columns.filter((c) => !pkColumns.includes(c));
9759
+ for (let offset = 0;offset < rows.length; offset += batchSize) {
9760
+ const batch = rows.slice(offset, offset + batchSize);
9761
+ try {
9762
+ if (isAsyncAdapter(target)) {
9763
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
9764
+ } else {
9765
+ batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
9766
+ }
9767
+ result.rowsWritten += batch.length;
9768
+ } catch (err) {
9769
+ result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
9770
+ }
9771
+ onProgress?.({
9772
+ table,
9773
+ phase: "writing",
9774
+ rowsRead: result.rowsRead,
9775
+ rowsWritten: result.rowsWritten,
9776
+ totalTables: tables.length,
9777
+ currentTableIndex: i
9778
+ });
9779
+ }
9780
+ onProgress?.({
9781
+ table,
9782
+ phase: "done",
9783
+ rowsRead: result.rowsRead,
9784
+ rowsWritten: result.rowsWritten,
9785
+ totalTables: tables.length,
9786
+ currentTableIndex: i
9787
+ });
9788
+ } catch (err) {
9789
+ result.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
9790
+ }
9791
+ results.push(result);
9792
+ }
9793
+ } finally {
9794
+ if (sqliteTarget) {
9795
+ try {
9796
+ sqliteTarget.exec("PRAGMA foreign_keys = ON");
9797
+ } catch {}
9798
+ try {
9799
+ const violations = sqliteTarget.all("PRAGMA foreign_key_check");
9800
+ if (violations.length > 0) {
9801
+ const tables2 = [...new Set(violations.map((v) => v.table))];
9802
+ const msg = `FK integrity check: ${violations.length} violation(s) in table(s): ${tables2.join(", ")}`;
9803
+ if (results.length > 0) {
9804
+ results[results.length - 1].errors.push(msg);
9805
+ }
9806
+ }
9807
+ } catch {}
9808
+ }
9809
+ }
9810
+ return results;
9811
+ }
9812
+ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
9813
+ if (batch.length === 0)
9814
+ return;
9815
+ const colList = columns.map((c) => `"${c}"`).join(", ");
9816
+ const valuePlaceholders = batch.map((_, rowIdx) => {
9817
+ const offset = rowIdx * columns.length;
9818
+ return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
9819
+ }).join(", ");
9820
+ const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
9821
+ const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
9822
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
9823
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
9824
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
9825
+ const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
9826
+ await target.run(sql, ...params);
9827
+ }
9828
+ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
9829
+ if (batch.length === 0)
9830
+ return;
9831
+ const colList = columns.map((c) => `"${c}"`).join(", ");
9832
+ const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
9833
+ const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
9834
+ const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
9835
+ const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
9836
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
9837
+ ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
9838
+ const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
9839
+ target.run(sql, ...params);
9840
+ }
9841
+ async function batchInsertPg(target, table, columns, batch) {
9842
+ if (batch.length === 0)
9843
+ return;
9844
+ const colList = columns.map((c) => `"${c}"`).join(", ");
9845
+ const valuePlaceholders = batch.map((_, rowIdx) => {
9846
+ const offset = rowIdx * columns.length;
9847
+ return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
9848
+ }).join(", ");
9849
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
9850
+ const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
9851
+ await target.run(sql, ...params);
9852
+ }
9853
+ function batchInsertSqlite(target, table, columns, batch) {
9854
+ if (batch.length === 0)
9855
+ return;
9856
+ const colList = columns.map((c) => `"${c}"`).join(", ");
9857
+ const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
9858
+ const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
9859
+ const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
9860
+ target.run(sql, ...params);
9861
+ }
9862
+ function coerceForSqlite(value) {
9863
+ if (value === null || value === undefined)
9864
+ return null;
9865
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
9866
+ return value;
9867
+ if (value instanceof Date)
9868
+ return value.toISOString();
9869
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
9870
+ return value;
9871
+ if (typeof value === "object")
9872
+ return JSON.stringify(value);
9873
+ return String(value);
9874
+ }
9875
+ function isAsyncAdapter(adapter) {
9876
+ return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
9877
+ }
9878
+ async function readAll(adapter, sql) {
9879
+ const result = adapter.all(sql);
9880
+ return result instanceof Promise ? await result : result;
9881
+ }
9882
+ function listSqliteTables(db) {
9883
+ const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
9884
+ return rows.map((r) => r.name);
9885
+ }
9385
9886
  init_config();
9887
+ var FEEDBACK_TABLE_SQL = `
9888
+ CREATE TABLE IF NOT EXISTS feedback (
9889
+ id TEXT PRIMARY KEY,
9890
+ service TEXT NOT NULL,
9891
+ version TEXT DEFAULT '',
9892
+ message TEXT NOT NULL,
9893
+ email TEXT DEFAULT '',
9894
+ machine_id TEXT DEFAULT '',
9895
+ created_at TEXT DEFAULT (datetime('now'))
9896
+ )`;
9897
+ function ensureFeedbackTable(db) {
9898
+ db.exec(FEEDBACK_TABLE_SQL);
9899
+ }
9900
+ function saveFeedback(db, feedback) {
9901
+ ensureFeedbackTable(db);
9902
+ const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
9903
+ const now = new Date().toISOString();
9904
+ const machineId = feedback.machine_id ?? hostname();
9905
+ db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
9906
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now);
9907
+ return id;
9908
+ }
9909
+ async function sendFeedback(feedback, db) {
9910
+ const config = getCloudConfig();
9911
+ const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
9912
+ const machineId = feedback.machine_id ?? hostname();
9913
+ const now = new Date().toISOString();
9914
+ const payload = {
9915
+ id,
9916
+ service: feedback.service,
9917
+ version: feedback.version ?? "",
9918
+ message: feedback.message,
9919
+ email: feedback.email ?? "",
9920
+ machine_id: machineId,
9921
+ created_at: feedback.created_at ?? now
9922
+ };
9923
+ try {
9924
+ const res = await fetch(config.feedback_endpoint, {
9925
+ method: "POST",
9926
+ headers: { "Content-Type": "application/json" },
9927
+ body: JSON.stringify(payload),
9928
+ signal: AbortSignal.timeout(1e4)
9929
+ });
9930
+ if (!res.ok) {
9931
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
9932
+ }
9933
+ if (db) {
9934
+ try {
9935
+ saveFeedback(db, { ...feedback, id });
9936
+ } catch {}
9937
+ }
9938
+ return { sent: true, id };
9939
+ } catch (err) {
9940
+ const errorMsg = err?.message ?? String(err);
9941
+ if (db) {
9942
+ try {
9943
+ saveFeedback(db, { ...feedback, id });
9944
+ } catch {}
9945
+ }
9946
+ return { sent: false, id, error: errorMsg };
9947
+ }
9948
+ }
9386
9949
  init_dotfile();
9387
9950
 
9388
9951
  class SyncProgressTracker {
@@ -9515,6 +10078,42 @@ init_config();
9515
10078
  var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
9516
10079
  init_adapter();
9517
10080
  init_config();
10081
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
10082
+ const pg2 = new PgAdapterAsync(connectionString);
10083
+ const result = {
10084
+ service,
10085
+ applied: [],
10086
+ alreadyApplied: [],
10087
+ errors: [],
10088
+ totalMigrations: migrations.length
10089
+ };
10090
+ try {
10091
+ await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
10092
+ id SERIAL PRIMARY KEY,
10093
+ version INT UNIQUE NOT NULL,
10094
+ applied_at TIMESTAMPTZ DEFAULT NOW()
10095
+ )`);
10096
+ const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
10097
+ const appliedSet = new Set(applied.map((r) => r.version));
10098
+ for (let i = 0;i < migrations.length; i++) {
10099
+ if (appliedSet.has(i)) {
10100
+ result.alreadyApplied.push(i);
10101
+ continue;
10102
+ }
10103
+ try {
10104
+ await pg2.exec(migrations[i]);
10105
+ await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
10106
+ result.applied.push(i);
10107
+ } catch (err) {
10108
+ result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
10109
+ break;
10110
+ }
10111
+ }
10112
+ } finally {
10113
+ await pg2.close();
10114
+ }
10115
+ return result;
10116
+ }
9518
10117
  init_discover();
9519
10118
  init_zod();
9520
10119
  init_config();
@@ -9525,563 +10124,515 @@ init_dotfile();
9525
10124
  init_adapter();
9526
10125
 
9527
10126
  // src/db/database.ts
9528
- import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
9529
- import { dirname as dirname2, join as join5 } from "path";
9530
-
9531
- // src/db/migrations.ts
10127
+ function defaultDatabasePath() {
10128
+ const baseDir = join5(process.env.HOME ?? ".", ".hasna", "invoices");
10129
+ mkdirSync3(baseDir, { recursive: true });
10130
+ return join5(baseDir, "invoices.db");
10131
+ }
10132
+ function openInvoiceDatabase(options = {}) {
10133
+ const dbPath = options.dbPath ?? defaultDatabasePath();
10134
+ mkdirSync3(dirname2(dbPath), { recursive: true });
10135
+ const db = new SqliteAdapter(dbPath);
10136
+ db.exec("PRAGMA journal_mode = WAL;");
10137
+ db.exec("PRAGMA foreign_keys = ON;");
10138
+ db.exec("PRAGMA busy_timeout = 5000;");
10139
+ return db;
10140
+ }
10141
+ // src/db/schema.ts
9532
10142
  var MIGRATIONS = [
9533
10143
  {
9534
- id: 1,
9535
- name: "initial_schema",
10144
+ id: "0001_init",
9536
10145
  sql: `
9537
- CREATE TABLE IF NOT EXISTS clients (
9538
- id TEXT PRIMARY KEY,
9539
- name TEXT NOT NULL,
9540
- email TEXT,
9541
- phone TEXT,
9542
- address TEXT,
9543
- tax_id TEXT,
9544
- notes TEXT,
9545
- metadata TEXT NOT NULL DEFAULT '{}',
9546
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
9547
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9548
- );
10146
+ CREATE TABLE IF NOT EXISTS migrations (
10147
+ id TEXT PRIMARY KEY,
10148
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
10149
+ );
9549
10150
 
9550
- CREATE TABLE IF NOT EXISTS invoices (
9551
- id TEXT PRIMARY KEY,
9552
- invoice_number TEXT NOT NULL UNIQUE,
9553
- client_id TEXT REFERENCES clients(id) ON DELETE SET NULL,
9554
- status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded')),
9555
- issue_date TEXT NOT NULL DEFAULT (date('now')),
9556
- due_date TEXT,
9557
- currency TEXT NOT NULL DEFAULT 'USD',
9558
- subtotal REAL NOT NULL DEFAULT 0,
9559
- tax_rate REAL NOT NULL DEFAULT 0,
9560
- tax_amount REAL NOT NULL DEFAULT 0,
9561
- discount REAL NOT NULL DEFAULT 0,
9562
- total REAL NOT NULL DEFAULT 0,
9563
- notes TEXT,
9564
- metadata TEXT NOT NULL DEFAULT '{}',
9565
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
9566
- updated_at TEXT NOT NULL DEFAULT (datetime('now')),
9567
- paid_at TEXT
9568
- );
10151
+ CREATE TABLE IF NOT EXISTS parties (
10152
+ id TEXT PRIMARY KEY,
10153
+ kind TEXT NOT NULL CHECK(kind IN ('issuer', 'customer')),
10154
+ legal_name TEXT NOT NULL,
10155
+ email TEXT,
10156
+ tax_id TEXT,
10157
+ country TEXT,
10158
+ address TEXT,
10159
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10160
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10161
+ );
9569
10162
 
9570
- CREATE TABLE IF NOT EXISTS line_items (
9571
- id TEXT PRIMARY KEY,
9572
- invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
9573
- description TEXT NOT NULL,
9574
- quantity REAL NOT NULL DEFAULT 1,
9575
- unit_price REAL NOT NULL DEFAULT 0,
9576
- amount REAL NOT NULL DEFAULT 0,
9577
- sort_order INTEGER NOT NULL DEFAULT 0,
9578
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
9579
- );
10163
+ CREATE TABLE IF NOT EXISTS invoices (
10164
+ id TEXT PRIMARY KEY,
10165
+ number TEXT NOT NULL UNIQUE,
10166
+ issuer_id TEXT NOT NULL REFERENCES parties(id),
10167
+ customer_id TEXT NOT NULL REFERENCES parties(id),
10168
+ status TEXT NOT NULL CHECK(status IN ('draft', 'sent', 'partially_paid', 'paid', 'void', 'overdue')),
10169
+ currency TEXT NOT NULL,
10170
+ notes TEXT,
10171
+ issued_at TEXT NOT NULL,
10172
+ due_at TEXT,
10173
+ subtotal_cents INTEGER NOT NULL DEFAULT 0,
10174
+ tax_cents INTEGER NOT NULL DEFAULT 0,
10175
+ total_cents INTEGER NOT NULL DEFAULT 0,
10176
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10177
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10178
+ );
9580
10179
 
9581
- CREATE TABLE IF NOT EXISTS payments (
9582
- id TEXT PRIMARY KEY,
9583
- invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
9584
- amount REAL NOT NULL,
9585
- method TEXT,
9586
- reference TEXT,
9587
- notes TEXT,
9588
- paid_at TEXT NOT NULL DEFAULT (datetime('now')),
9589
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
9590
- );
10180
+ CREATE TABLE IF NOT EXISTS invoice_lines (
10181
+ id TEXT PRIMARY KEY,
10182
+ invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
10183
+ position INTEGER NOT NULL,
10184
+ description TEXT NOT NULL,
10185
+ quantity REAL NOT NULL DEFAULT 1,
10186
+ unit_price_cents INTEGER NOT NULL DEFAULT 0,
10187
+ tax_rate_basis_points INTEGER NOT NULL DEFAULT 0,
10188
+ line_total_cents INTEGER NOT NULL DEFAULT 0,
10189
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10190
+ );
9591
10191
 
9592
- CREATE TABLE IF NOT EXISTS invoice_counter (
9593
- id INTEGER PRIMARY KEY CHECK (id = 1),
9594
- prefix TEXT NOT NULL DEFAULT 'INV',
9595
- next_number INTEGER NOT NULL DEFAULT 1
9596
- );
10192
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_lines_invoice_position
10193
+ ON invoice_lines(invoice_id, position);
9597
10194
 
9598
- INSERT OR IGNORE INTO invoice_counter (id, prefix, next_number) VALUES (1, 'INV', 1);
10195
+ CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(number);
10196
+ CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
10197
+ CREATE INDEX IF NOT EXISTS idx_invoices_issued_at ON invoices(issued_at);
10198
+ CREATE INDEX IF NOT EXISTS idx_invoices_due_at ON invoices(due_at);
9599
10199
 
9600
- CREATE INDEX IF NOT EXISTS idx_invoices_client ON invoices(client_id);
9601
- CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
9602
- CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(invoice_number);
9603
- CREATE INDEX IF NOT EXISTS idx_invoices_due ON invoices(due_date);
9604
- CREATE INDEX IF NOT EXISTS idx_line_items_invoice ON line_items(invoice_id);
9605
- CREATE INDEX IF NOT EXISTS idx_payments_invoice ON payments(invoice_id);
9606
- CREATE INDEX IF NOT EXISTS idx_clients_name ON clients(name);
9607
- CREATE INDEX IF NOT EXISTS idx_clients_email ON clients(email);
9608
- `
9609
- },
9610
- {
9611
- id: 2,
9612
- name: "multi_country_support",
9613
- sql: `
9614
- CREATE TABLE IF NOT EXISTS business_profiles (
9615
- id TEXT PRIMARY KEY,
9616
- name TEXT NOT NULL,
9617
- address_line1 TEXT,
9618
- address_line2 TEXT,
9619
- city TEXT,
9620
- state TEXT,
9621
- postal_code TEXT,
9622
- country TEXT NOT NULL DEFAULT 'US',
9623
- tax_id TEXT,
9624
- vat_number TEXT,
9625
- registration_number TEXT,
9626
- email TEXT,
9627
- phone TEXT,
9628
- website TEXT,
9629
- bank_name TEXT,
9630
- bank_iban TEXT,
9631
- bank_swift TEXT,
9632
- bank_account TEXT,
9633
- logo_url TEXT,
9634
- is_default INTEGER NOT NULL DEFAULT 0,
9635
- metadata TEXT NOT NULL DEFAULT '{}',
9636
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
9637
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
9638
- );
10200
+ CREATE VIRTUAL TABLE IF NOT EXISTS invoices_fts USING fts5(
10201
+ invoice_id UNINDEXED,
10202
+ number,
10203
+ issuer_name,
10204
+ customer_name,
10205
+ notes
10206
+ );
9639
10207
 
9640
- CREATE TABLE IF NOT EXISTS tax_rules (
9641
- id TEXT PRIMARY KEY,
9642
- country TEXT NOT NULL,
9643
- region TEXT,
9644
- tax_name TEXT NOT NULL,
9645
- rate REAL NOT NULL,
9646
- type TEXT NOT NULL DEFAULT 'vat' CHECK(type IN ('vat', 'sales_tax', 'gst', 'other')),
9647
- is_default INTEGER NOT NULL DEFAULT 0,
9648
- reverse_charge INTEGER NOT NULL DEFAULT 0,
9649
- description TEXT,
9650
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
9651
- );
10208
+ CREATE TRIGGER IF NOT EXISTS invoices_ai_fts
10209
+ AFTER INSERT ON invoices
10210
+ BEGIN
10211
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
10212
+ VALUES (
10213
+ NEW.id,
10214
+ NEW.number,
10215
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
10216
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
10217
+ COALESCE(NEW.notes, '')
10218
+ );
10219
+ END;
9652
10220
 
9653
- -- Add new columns to invoices
9654
- ALTER TABLE invoices ADD COLUMN business_profile_id TEXT REFERENCES business_profiles(id) ON DELETE SET NULL;
9655
- ALTER TABLE invoices ADD COLUMN tax_name TEXT DEFAULT 'Tax';
9656
- ALTER TABLE invoices ADD COLUMN reverse_charge INTEGER NOT NULL DEFAULT 0;
9657
- ALTER TABLE invoices ADD COLUMN language TEXT NOT NULL DEFAULT 'en';
9658
- ALTER TABLE invoices ADD COLUMN footer_text TEXT;
10221
+ CREATE TRIGGER IF NOT EXISTS invoices_au_fts
10222
+ AFTER UPDATE ON invoices
10223
+ BEGIN
10224
+ DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
10225
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
10226
+ VALUES (
10227
+ NEW.id,
10228
+ NEW.number,
10229
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
10230
+ COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
10231
+ COALESCE(NEW.notes, '')
10232
+ );
10233
+ END;
9659
10234
 
9660
- -- Add new columns to clients
9661
- ALTER TABLE clients ADD COLUMN address_line1 TEXT;
9662
- ALTER TABLE clients ADD COLUMN address_line2 TEXT;
9663
- ALTER TABLE clients ADD COLUMN city TEXT;
9664
- ALTER TABLE clients ADD COLUMN state TEXT;
9665
- ALTER TABLE clients ADD COLUMN postal_code TEXT;
9666
- ALTER TABLE clients ADD COLUMN country TEXT DEFAULT 'US';
9667
- ALTER TABLE clients ADD COLUMN vat_number TEXT;
9668
- ALTER TABLE clients ADD COLUMN language TEXT DEFAULT 'en';
9669
-
9670
- -- Add per-line-item tax support
9671
- ALTER TABLE line_items ADD COLUMN tax_rate REAL;
9672
- ALTER TABLE line_items ADD COLUMN tax_amount REAL DEFAULT 0;
10235
+ CREATE TRIGGER IF NOT EXISTS invoices_ad_fts
10236
+ AFTER DELETE ON invoices
10237
+ BEGIN
10238
+ DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
10239
+ END;
10240
+ `
10241
+ },
10242
+ {
10243
+ id: "0002_party_fts_sync",
10244
+ sql: `
10245
+ CREATE TRIGGER IF NOT EXISTS parties_au_invoice_fts
10246
+ AFTER UPDATE ON parties
10247
+ BEGIN
10248
+ DELETE FROM invoices_fts
10249
+ WHERE invoice_id IN (
10250
+ SELECT id FROM invoices WHERE issuer_id = NEW.id OR customer_id = NEW.id
10251
+ );
9673
10252
 
9674
- -- Seed default tax rules
9675
- INSERT INTO tax_rules (id, country, tax_name, rate, type, is_default, description) VALUES
9676
- ('tax-ro-vat-19', 'RO', 'TVA', 19, 'vat', 1, 'Romania standard VAT 19%'),
9677
- ('tax-ro-vat-9', 'RO', 'TVA', 9, 'vat', 0, 'Romania reduced VAT 9% (food, hotels)'),
9678
- ('tax-ro-vat-5', 'RO', 'TVA', 5, 'vat', 0, 'Romania reduced VAT 5% (housing)'),
9679
- ('tax-us-none', 'US', 'Sales Tax', 0, 'sales_tax', 1, 'US federal (no federal sales tax)'),
9680
- ('tax-us-ca', 'US', 'Sales Tax', 7.25, 'sales_tax', 0, 'California base sales tax'),
9681
- ('tax-us-ny', 'US', 'Sales Tax', 8, 'sales_tax', 0, 'New York sales tax'),
9682
- ('tax-us-tx', 'US', 'Sales Tax', 6.25, 'sales_tax', 0, 'Texas sales tax'),
9683
- ('tax-uk-vat-20', 'GB', 'VAT', 20, 'vat', 1, 'UK standard VAT 20%'),
9684
- ('tax-uk-vat-5', 'GB', 'VAT', 5, 'vat', 0, 'UK reduced VAT 5%'),
9685
- ('tax-uk-vat-0', 'GB', 'VAT', 0, 'vat', 0, 'UK zero-rated VAT'),
9686
- ('tax-de-vat-19', 'DE', 'MwSt', 19, 'vat', 1, 'Germany standard VAT 19%'),
9687
- ('tax-de-vat-7', 'DE', 'MwSt', 7, 'vat', 0, 'Germany reduced VAT 7%'),
9688
- ('tax-fr-vat-20', 'FR', 'TVA', 20, 'vat', 1, 'France standard VAT 20%'),
9689
- ('tax-fr-vat-10', 'FR', 'TVA', 10, 'vat', 0, 'France reduced VAT 10%'),
9690
- ('tax-fr-vat-55', 'FR', 'TVA', 5.5, 'vat', 0, 'France reduced VAT 5.5%'),
9691
- ('tax-nl-vat-21', 'NL', 'BTW', 21, 'vat', 1, 'Netherlands standard VAT 21%'),
9692
- ('tax-it-vat-22', 'IT', 'IVA', 22, 'vat', 1, 'Italy standard VAT 22%'),
9693
- ('tax-es-vat-21', 'ES', 'IVA', 21, 'vat', 1, 'Spain standard VAT 21%'),
9694
- ('tax-at-vat-20', 'AT', 'USt', 20, 'vat', 1, 'Austria standard VAT 20%'),
9695
- ('tax-be-vat-21', 'BE', 'BTW', 21, 'vat', 1, 'Belgium standard VAT 21%'),
9696
- ('tax-pl-vat-23', 'PL', 'VAT', 23, 'vat', 1, 'Poland standard VAT 23%'),
9697
- ('tax-ie-vat-23', 'IE', 'VAT', 23, 'vat', 1, 'Ireland standard VAT 23%'),
9698
- ('tax-se-vat-25', 'SE', 'Moms', 25, 'vat', 1, 'Sweden standard VAT 25%'),
9699
- ('tax-dk-vat-25', 'DK', 'Moms', 25, 'vat', 1, 'Denmark standard VAT 25%'),
9700
- ('tax-hu-vat-27', 'HU', 'AFA', 27, 'vat', 1, 'Hungary standard VAT 27%'),
9701
- ('tax-bg-vat-20', 'BG', 'DDC', 20, 'vat', 1, 'Bulgaria standard VAT 20%'),
9702
- ('tax-eu-rc', 'EU', 'Reverse Charge', 0, 'vat', 0, 'EU B2B reverse charge mechanism');
10253
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
10254
+ SELECT
10255
+ i.id,
10256
+ i.number,
10257
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
10258
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
10259
+ COALESCE(i.notes, '')
10260
+ FROM invoices i
10261
+ WHERE i.issuer_id = NEW.id OR i.customer_id = NEW.id;
10262
+ END;
9703
10263
 
9704
- CREATE INDEX IF NOT EXISTS idx_business_profiles_default ON business_profiles(is_default);
9705
- CREATE INDEX IF NOT EXISTS idx_tax_rules_country ON tax_rules(country);
9706
- CREATE INDEX IF NOT EXISTS idx_invoices_business ON invoices(business_profile_id);
9707
- `
10264
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
10265
+ SELECT
10266
+ i.id,
10267
+ i.number,
10268
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
10269
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
10270
+ COALESCE(i.notes, '')
10271
+ FROM invoices i
10272
+ WHERE i.id NOT IN (SELECT invoice_id FROM invoices_fts);
10273
+ `
10274
+ },
10275
+ {
10276
+ id: "0003_agents",
10277
+ sql: `
10278
+ CREATE TABLE IF NOT EXISTS agents (
10279
+ id TEXT PRIMARY KEY,
10280
+ name TEXT NOT NULL UNIQUE,
10281
+ description TEXT,
10282
+ focus TEXT,
10283
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10284
+ last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
10285
+ );
10286
+ `
9708
10287
  }
9709
10288
  ];
9710
10289
 
9711
- // src/db/database.ts
9712
- var _db = null;
9713
- function getDbPath2() {
9714
- const explicit = process.env["HASNA_INVOICES_DIR"] ?? process.env["INVOICES_DIR"];
9715
- if (explicit) {
9716
- return join5(explicit, "invoices.db");
9717
- }
9718
- const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "~";
9719
- return join5(home, ".hasna", "invoices", "invoices.db");
10290
+ // src/db/migrate.ts
10291
+ function ensureMigrationsTable(db) {
10292
+ db.exec(`
10293
+ CREATE TABLE IF NOT EXISTS migrations (
10294
+ id TEXT PRIMARY KEY,
10295
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
10296
+ );
10297
+ `);
9720
10298
  }
9721
- function getDatabase() {
9722
- if (_db)
9723
- return _db;
9724
- const dbPath = getDbPath2();
9725
- const dir = dirname2(dbPath);
9726
- if (!existsSync4(dir)) {
9727
- mkdirSync3(dir, { recursive: true });
9728
- }
9729
- const adapter = new SqliteAdapter(dbPath);
9730
- _db = adapter.raw;
9731
- _db.exec(`
9732
- CREATE TABLE IF NOT EXISTS _migrations (
9733
- id INTEGER PRIMARY KEY,
9734
- name TEXT NOT NULL,
9735
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
9736
- )
9737
- `);
9738
- const applied = _db.query("SELECT id FROM _migrations ORDER BY id").all();
9739
- const appliedIds = new Set(applied.map((r) => r.id));
10299
+ function applyMigrations(db) {
10300
+ ensureMigrationsTable(db);
10301
+ const getMigration = db.query("SELECT id FROM migrations WHERE id = ?1");
10302
+ const markMigration = db.query("INSERT INTO migrations (id) VALUES (?1)");
10303
+ const applied = [];
9740
10304
  for (const migration of MIGRATIONS) {
9741
- if (appliedIds.has(migration.id))
10305
+ const row = getMigration.get(migration.id);
10306
+ if (row) {
9742
10307
  continue;
9743
- _db.exec("BEGIN");
9744
- try {
9745
- _db.exec(migration.sql);
9746
- _db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(migration.id, migration.name);
9747
- _db.exec("COMMIT");
9748
- } catch (error) {
9749
- _db.exec("ROLLBACK");
9750
- throw new Error(`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`);
9751
10308
  }
10309
+ db.transaction(() => {
10310
+ db.exec(migration.sql);
10311
+ markMigration.run(migration.id);
10312
+ });
10313
+ applied.push(migration.id);
9752
10314
  }
9753
- return _db;
10315
+ return applied;
9754
10316
  }
9755
- function closeDatabase() {
9756
- if (_db) {
9757
- _db.close();
9758
- _db = null;
10317
+ function migrateDatabase(options = {}) {
10318
+ const db = openInvoiceDatabase(options);
10319
+ try {
10320
+ return applyMigrations(db);
10321
+ } finally {
10322
+ db.close();
9759
10323
  }
9760
10324
  }
9761
-
9762
- // src/db/invoices.ts
9763
- function rowToInvoice(row) {
9764
- return {
9765
- ...row,
9766
- reverse_charge: row.reverse_charge === 1,
9767
- tax_name: row["tax_name"] || "Tax",
9768
- language: row["language"] || "en",
9769
- footer_text: row["footer_text"] ?? null,
9770
- business_profile_id: row["business_profile_id"] ?? null,
9771
- metadata: JSON.parse(row.metadata || "{}")
9772
- };
9773
- }
9774
- function nextInvoiceNumber() {
9775
- const db = getDatabase();
9776
- const counter = db.prepare("SELECT prefix, next_number FROM invoice_counter WHERE id = 1").get();
9777
- const number = `${counter.prefix}-${String(counter.next_number).padStart(5, "0")}`;
9778
- db.prepare("UPDATE invoice_counter SET next_number = next_number + 1 WHERE id = 1").run();
9779
- return number;
10325
+ // src/db/pg.ts
10326
+ function defaultPgConnectionString() {
10327
+ return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
9780
10328
  }
9781
- function recalculateInvoice(invoiceId) {
9782
- const db = getDatabase();
9783
- const row = db.prepare("SELECT COALESCE(SUM(amount), 0) as subtotal FROM line_items WHERE invoice_id = ?").get(invoiceId);
9784
- const invoice = db.prepare("SELECT tax_rate, discount FROM invoices WHERE id = ?").get(invoiceId);
9785
- const subtotal = row.subtotal;
9786
- const discounted = subtotal - (invoice.discount || 0);
9787
- const taxAmount = discounted * ((invoice.tax_rate || 0) / 100);
9788
- const total = discounted + taxAmount;
9789
- db.prepare("UPDATE invoices SET subtotal = ?, tax_amount = ?, total = ?, updated_at = datetime('now') WHERE id = ?").run(subtotal, taxAmount, total, invoiceId);
10329
+ async function migratePgDatabase(connectionString = defaultPgConnectionString()) {
10330
+ const pgMigrations = MIGRATIONS.map((migration) => translateDdl(migration.sql, "pg"));
10331
+ return applyPgMigrations(connectionString, pgMigrations, "invoices");
9790
10332
  }
9791
- function createInvoice(input = {}) {
9792
- const db = getDatabase();
9793
- const id = crypto.randomUUID();
9794
- const invoiceNumber = input.invoice_number || nextInvoiceNumber();
9795
- db.prepare(`INSERT INTO invoices (id, invoice_number, client_id, business_profile_id, issue_date, due_date, currency, tax_rate, tax_name, discount, reverse_charge, language, notes, footer_text)
9796
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, invoiceNumber, input.client_id || null, input.business_profile_id || null, input.issue_date ?? new Date().toISOString().split("T")[0], input.due_date ?? null, input.currency || "USD", input.tax_rate || 0, input.tax_name || "Tax", input.discount || 0, input.reverse_charge ? 1 : 0, input.language || "en", input.notes || null, input.footer_text || null);
9797
- return getInvoice(id);
10333
+ // src/db/cloud.ts
10334
+ function defaultConnectionString() {
10335
+ return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
9798
10336
  }
9799
- function getInvoice(id) {
9800
- const db = getDatabase();
9801
- const row = db.prepare("SELECT * FROM invoices WHERE id = ? OR invoice_number = ?").get(id, id);
9802
- return row ? rowToInvoice(row) : null;
10337
+ function parseTablesArg(tables) {
10338
+ if (!tables)
10339
+ return [];
10340
+ return tables.split(",").map((table) => table.trim()).filter(Boolean);
9803
10341
  }
9804
- function getInvoiceWithItems(id) {
9805
- const invoice = getInvoice(id);
9806
- if (!invoice)
9807
- return null;
9808
- const db = getDatabase();
9809
- const line_items = db.prepare("SELECT * FROM line_items WHERE invoice_id = ? ORDER BY sort_order").all(invoice.id);
9810
- const payments = db.prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY paid_at").all(invoice.id);
9811
- return { ...invoice, line_items, payments };
10342
+ function resolveTables(local, tablesCsv) {
10343
+ const configured = parseTablesArg(tablesCsv);
10344
+ return configured.length > 0 ? configured : listSqliteTables(local);
9812
10345
  }
9813
- function listInvoices(options = {}) {
9814
- const db = getDatabase();
9815
- const conditions = [];
9816
- const params = [];
9817
- if (options.status) {
9818
- conditions.push("status = ?");
9819
- params.push(options.status);
9820
- }
9821
- if (options.client_id) {
9822
- conditions.push("client_id = ?");
9823
- params.push(options.client_id);
9824
- }
9825
- if (options.from_date) {
9826
- conditions.push("issue_date >= ?");
9827
- params.push(options.from_date);
9828
- }
9829
- if (options.to_date) {
9830
- conditions.push("issue_date <= ?");
9831
- params.push(options.to_date);
9832
- }
9833
- let sql = "SELECT * FROM invoices";
9834
- if (conditions.length > 0) {
9835
- sql += " WHERE " + conditions.join(" AND ");
10346
+ async function cloudPush(local, tablesCsv, connectionString = defaultConnectionString()) {
10347
+ const remote = new PgAdapterAsync(connectionString);
10348
+ try {
10349
+ const tables = resolveTables(local, tablesCsv);
10350
+ const result = await syncPush(local, remote, { tables });
10351
+ return { direction: "push", tables, result };
10352
+ } finally {
10353
+ await remote.close();
9836
10354
  }
9837
- sql += " ORDER BY created_at DESC";
9838
- if (options.limit) {
9839
- sql += " LIMIT ?";
9840
- params.push(options.limit);
10355
+ }
10356
+ async function cloudPull(local, tablesCsv, connectionString = defaultConnectionString()) {
10357
+ const remote = new PgAdapterAsync(connectionString);
10358
+ try {
10359
+ const tables = resolveTables(local, tablesCsv);
10360
+ const result = await syncPull(remote, local, { tables });
10361
+ return { direction: "pull", tables, result };
10362
+ } finally {
10363
+ await remote.close();
9841
10364
  }
9842
- const rows = db.prepare(sql).all(...params);
9843
- return rows.map(rowToInvoice);
9844
10365
  }
9845
- function updateInvoiceStatus(id, status) {
9846
- const db = getDatabase();
9847
- const paidAt = status === "paid" ? "datetime('now')" : "NULL";
9848
- db.prepare(`UPDATE invoices SET status = ?, paid_at = ${paidAt}, updated_at = datetime('now') WHERE id = ?`).run(status, id);
9849
- return getInvoice(id);
10366
+ async function submitFeedback(local, feedback) {
10367
+ const payload = {
10368
+ service: "invoices",
10369
+ version: feedback.version,
10370
+ message: feedback.message,
10371
+ email: feedback.email,
10372
+ machine_id: feedback.machine_id
10373
+ };
10374
+ const delivery = await sendFeedback(payload, local);
10375
+ if (!delivery.sent) {
10376
+ const id = saveFeedback(local, payload);
10377
+ return { ...delivery, id, stored_locally: true };
10378
+ }
10379
+ return { ...delivery, stored_locally: false };
9850
10380
  }
9851
- function deleteInvoice(id) {
9852
- const db = getDatabase();
9853
- return db.prepare("DELETE FROM invoices WHERE id = ?").run(id).changes > 0;
10381
+ // src/db/invoices.ts
10382
+ import { randomUUID } from "crypto";
10383
+ function toLineTotalCents(line) {
10384
+ const subtotal = Math.round(line.quantity * line.unitPriceCents);
10385
+ const taxRate = line.taxRateBasisPoints ?? 0;
10386
+ const tax = Math.round(subtotal * taxRate / 1e4);
10387
+ return { lineTotalCents: subtotal, lineTaxCents: tax };
9854
10388
  }
9855
- function addLineItem(input) {
9856
- const db = getDatabase();
9857
- const id = crypto.randomUUID();
9858
- const quantity = input.quantity || 1;
9859
- const amount = quantity * input.unit_price;
9860
- const maxOrder = db.prepare("SELECT COALESCE(MAX(sort_order), -1) as max_order FROM line_items WHERE invoice_id = ?").get(input.invoice_id);
9861
- db.prepare(`INSERT INTO line_items (id, invoice_id, description, quantity, unit_price, amount, sort_order)
9862
- VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.description, quantity, input.unit_price, amount, maxOrder.max_order + 1);
9863
- recalculateInvoice(input.invoice_id);
9864
- return db.prepare("SELECT * FROM line_items WHERE id = ?").get(id);
10389
+ function mapPartyRow(row) {
10390
+ return {
10391
+ id: row.id,
10392
+ kind: row.kind,
10393
+ legalName: row.legal_name,
10394
+ email: row.email ?? undefined,
10395
+ taxId: row.tax_id ?? undefined,
10396
+ country: row.country ?? undefined,
10397
+ address: row.address ?? undefined,
10398
+ createdAt: row.created_at,
10399
+ updatedAt: row.updated_at
10400
+ };
9865
10401
  }
9866
- function removeLineItem(id) {
9867
- const db = getDatabase();
9868
- const item = db.prepare("SELECT invoice_id FROM line_items WHERE id = ?").get(id);
9869
- if (!item)
9870
- return false;
9871
- db.prepare("DELETE FROM line_items WHERE id = ?").run(id);
9872
- recalculateInvoice(item.invoice_id);
9873
- return true;
10402
+ function mapInvoiceRow(row) {
10403
+ return {
10404
+ id: row.id,
10405
+ number: row.number,
10406
+ issuerId: row.issuer_id,
10407
+ customerId: row.customer_id,
10408
+ status: row.status,
10409
+ currency: row.currency,
10410
+ notes: row.notes ?? undefined,
10411
+ issuedAt: row.issued_at,
10412
+ dueAt: row.due_at ?? undefined,
10413
+ subtotalCents: row.subtotal_cents,
10414
+ taxCents: row.tax_cents,
10415
+ totalCents: row.total_cents,
10416
+ createdAt: row.created_at,
10417
+ updatedAt: row.updated_at
10418
+ };
9874
10419
  }
9875
- function recordPayment(input) {
9876
- const db = getDatabase();
9877
- const id = crypto.randomUUID();
9878
- db.prepare(`INSERT INTO payments (id, invoice_id, amount, method, reference, notes)
9879
- VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.amount, input.method || null, input.reference || null, input.notes || null);
9880
- const invoice = getInvoice(input.invoice_id);
9881
- if (invoice) {
9882
- const totalPaid = db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE invoice_id = ?").get(input.invoice_id);
9883
- if (totalPaid.total >= invoice.total) {
9884
- updateInvoiceStatus(input.invoice_id, "paid");
9885
- }
9886
- }
9887
- return db.prepare("SELECT * FROM payments WHERE id = ?").get(id);
10420
+ function mapInvoiceLineRow(row) {
10421
+ return {
10422
+ id: row.id,
10423
+ invoiceId: row.invoice_id,
10424
+ position: row.position,
10425
+ description: row.description,
10426
+ quantity: row.quantity,
10427
+ unitPriceCents: row.unit_price_cents,
10428
+ taxRateBasisPoints: row.tax_rate_basis_points,
10429
+ lineTotalCents: row.line_total_cents,
10430
+ createdAt: row.created_at
10431
+ };
9888
10432
  }
9889
- function getInvoiceSummary() {
9890
- const db = getDatabase();
9891
- const counts = db.prepare(`SELECT
9892
- COUNT(*) as total_invoices,
9893
- SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END) as draft,
9894
- SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END) as sent,
9895
- SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
9896
- SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) as overdue,
9897
- COALESCE(SUM(CASE WHEN status IN ('sent', 'overdue') THEN total ELSE 0 END), 0) as total_outstanding,
9898
- COALESCE(SUM(CASE WHEN status = 'paid' THEN total ELSE 0 END), 0) as total_paid
9899
- FROM invoices`).get();
9900
- return counts;
10433
+ function createParty(db, input) {
10434
+ const id = input.id ?? randomUUID();
10435
+ db.query(`
10436
+ INSERT INTO parties (id, kind, legal_name, email, tax_id, country, address)
10437
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
10438
+ `).run(id, input.kind, input.legalName, input.email ?? null, input.taxId ?? null, input.country ?? null, input.address ?? null);
10439
+ return getPartyById(db, id);
9901
10440
  }
9902
- // src/db/clients.ts
9903
- function rowToClient(row) {
9904
- return { ...row, metadata: JSON.parse(row.metadata || "{}") };
10441
+ function updateParty(db, id, updates) {
10442
+ db.query(`
10443
+ UPDATE parties
10444
+ SET legal_name = COALESCE(?2, legal_name),
10445
+ email = COALESCE(?3, email),
10446
+ tax_id = COALESCE(?4, tax_id),
10447
+ country = COALESCE(?5, country),
10448
+ address = COALESCE(?6, address),
10449
+ updated_at = datetime('now')
10450
+ WHERE id = ?1
10451
+ `).run(id, updates.legalName ?? null, updates.email ?? null, updates.taxId ?? null, updates.country ?? null, updates.address ?? null);
10452
+ return getPartyById(db, id);
9905
10453
  }
9906
- function createClient(input) {
9907
- const db = getDatabase();
9908
- const id = crypto.randomUUID();
9909
- db.prepare(`INSERT INTO clients (id, name, email, phone, address, address_line1, address_line2, city, state, postal_code, country, tax_id, vat_number, language, notes)
9910
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.name, input.email || null, input.phone || null, input.address || null, input.address_line1 || null, input.address_line2 || null, input.city || null, input.state || null, input.postal_code || null, input.country || null, input.tax_id || null, input.vat_number || null, input.language || null, input.notes || null);
9911
- return getClient(id);
10454
+ function getPartyById(db, id) {
10455
+ const row = db.query("SELECT * FROM parties WHERE id = ?1").get(id);
10456
+ return row ? mapPartyRow(row) : null;
9912
10457
  }
9913
- function getClient(id) {
9914
- const db = getDatabase();
9915
- const row = db.prepare("SELECT * FROM clients WHERE id = ?").get(id);
9916
- return row ? rowToClient(row) : null;
10458
+ function listParties(db, kind) {
10459
+ if (kind) {
10460
+ return db.query("SELECT * FROM parties WHERE kind = ?1 ORDER BY legal_name ASC").all(kind).map(mapPartyRow);
10461
+ }
10462
+ return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
9917
10463
  }
9918
- function listClients(search) {
9919
- const db = getDatabase();
9920
- if (search) {
9921
- const q = `%${search}%`;
9922
- const rows = db.prepare("SELECT * FROM clients WHERE name LIKE ? OR email LIKE ? ORDER BY name").all(q, q);
9923
- return rows.map(rowToClient);
9924
- }
9925
- return db.prepare("SELECT * FROM clients ORDER BY name").all().map(rowToClient);
10464
+ function createInvoice(db, input) {
10465
+ const invoiceId = input.id ?? randomUUID();
10466
+ const preparedLines = input.lines.map((line, index) => {
10467
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
10468
+ return {
10469
+ id: line.id ?? randomUUID(),
10470
+ position: index,
10471
+ description: line.description,
10472
+ quantity: line.quantity,
10473
+ unitPriceCents: line.unitPriceCents,
10474
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
10475
+ lineTotalCents,
10476
+ lineTaxCents
10477
+ };
10478
+ });
10479
+ const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
10480
+ const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
10481
+ const totalCents = subtotalCents + taxCents;
10482
+ db.transaction(() => {
10483
+ db.query(`
10484
+ INSERT INTO invoices (
10485
+ id, number, issuer_id, customer_id, status, currency, notes, issued_at, due_at, subtotal_cents, tax_cents, total_cents
10486
+ )
10487
+ VALUES (?1, ?2, ?3, ?4, 'draft', ?5, ?6, ?7, ?8, ?9, ?10, ?11)
10488
+ `).run(invoiceId, input.number, input.issuerId, input.customerId, input.currency, input.notes ?? null, input.issuedAt, input.dueAt ?? null, subtotalCents, taxCents, totalCents);
10489
+ const insertLine = db.query(`
10490
+ INSERT INTO invoice_lines (
10491
+ id, invoice_id, position, description, quantity, unit_price_cents, tax_rate_basis_points, line_total_cents
10492
+ )
10493
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
10494
+ `);
10495
+ for (const line of preparedLines) {
10496
+ insertLine.run(line.id, invoiceId, line.position, line.description, line.quantity, line.unitPriceCents, line.taxRateBasisPoints, line.lineTotalCents);
10497
+ }
10498
+ });
10499
+ return getInvoiceById(db, invoiceId);
9926
10500
  }
9927
- function updateClient(id, input) {
9928
- const db = getDatabase();
9929
- const existing = getClient(id);
9930
- if (!existing)
10501
+ function getInvoiceById(db, id) {
10502
+ const invoiceRow = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
10503
+ if (!invoiceRow) {
9931
10504
  return null;
9932
- const sets = [];
9933
- const params = [];
9934
- for (const [key, value] of Object.entries(input)) {
9935
- if (value !== undefined) {
9936
- sets.push(`${key} = ?`);
9937
- params.push(value);
9938
- }
9939
- }
9940
- if (sets.length === 0)
9941
- return existing;
9942
- sets.push("updated_at = datetime('now')");
9943
- params.push(id);
9944
- db.prepare(`UPDATE clients SET ${sets.join(", ")} WHERE id = ?`).run(...params);
9945
- return getClient(id);
9946
- }
9947
- function deleteClient(id) {
9948
- const db = getDatabase();
9949
- return db.prepare("DELETE FROM clients WHERE id = ?").run(id).changes > 0;
9950
- }
9951
- // src/db/business.ts
9952
- function rowToBusiness(row) {
10505
+ }
10506
+ const lines = db.query("SELECT * FROM invoice_lines WHERE invoice_id = ?1 ORDER BY position ASC").all(id).map(mapInvoiceLineRow);
9953
10507
  return {
9954
- ...row,
9955
- is_default: row.is_default === 1,
9956
- metadata: JSON.parse(row.metadata || "{}")
10508
+ ...mapInvoiceRow(invoiceRow),
10509
+ lines
9957
10510
  };
9958
10511
  }
9959
- function createBusinessProfile(input) {
9960
- const db = getDatabase();
9961
- const id = crypto.randomUUID();
9962
- if (input.is_default) {
9963
- db.prepare("UPDATE business_profiles SET is_default = 0").run();
9964
- }
9965
- db.prepare(`INSERT INTO business_profiles (id, name, address_line1, address_line2, city, state, postal_code, country, tax_id, vat_number, registration_number, email, phone, website, bank_name, bank_iban, bank_swift, bank_account, logo_url, is_default)
9966
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.name, input.address_line1 || null, input.address_line2 || null, input.city || null, input.state || null, input.postal_code || null, input.country || "US", input.tax_id || null, input.vat_number || null, input.registration_number || null, input.email || null, input.phone || null, input.website || null, input.bank_name || null, input.bank_iban || null, input.bank_swift || null, input.bank_account || null, input.logo_url || null, input.is_default ? 1 : 0);
9967
- return getBusinessProfile(id);
9968
- }
9969
- function getBusinessProfile(id) {
9970
- const db = getDatabase();
9971
- const row = db.prepare("SELECT * FROM business_profiles WHERE id = ?").get(id);
9972
- return row ? rowToBusiness(row) : null;
9973
- }
9974
- function getDefaultBusinessProfile() {
9975
- const db = getDatabase();
9976
- const row = db.prepare("SELECT * FROM business_profiles WHERE is_default = 1").get();
9977
- return row ? rowToBusiness(row) : null;
10512
+ function listInvoices(db, options = {}) {
10513
+ const limit = options.limit ?? 50;
10514
+ const offset = options.offset ?? 0;
10515
+ if (options.status) {
10516
+ return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
10517
+ }
10518
+ return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
9978
10519
  }
9979
- function listBusinessProfiles() {
9980
- const db = getDatabase();
9981
- return db.prepare("SELECT * FROM business_profiles ORDER BY is_default DESC, name").all().map(rowToBusiness);
10520
+ function updateInvoiceStatus(db, id, status) {
10521
+ db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
10522
+ const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
10523
+ return row ? mapInvoiceRow(row) : null;
9982
10524
  }
9983
- function updateBusinessProfile(id, input) {
9984
- const db = getDatabase();
9985
- if (!getBusinessProfile(id))
9986
- return null;
9987
- const sets = [];
9988
- const params = [];
9989
- for (const [key, value] of Object.entries(input)) {
9990
- if (value === undefined)
9991
- continue;
9992
- if (key === "is_default") {
9993
- if (value)
9994
- db.prepare("UPDATE business_profiles SET is_default = 0").run();
9995
- sets.push("is_default = ?");
9996
- params.push(value ? 1 : 0);
9997
- } else {
9998
- sets.push(`${key} = ?`);
9999
- params.push(value);
10000
- }
10001
- }
10002
- if (sets.length === 0)
10003
- return getBusinessProfile(id);
10004
- sets.push("updated_at = datetime('now')");
10005
- params.push(id);
10006
- db.prepare(`UPDATE business_profiles SET ${sets.join(", ")} WHERE id = ?`).run(...params);
10007
- return getBusinessProfile(id);
10525
+ function deleteInvoice(db, id) {
10526
+ const result = db.query("DELETE FROM invoices WHERE id = ?1").run(id);
10527
+ return result.changes > 0;
10008
10528
  }
10009
- function deleteBusinessProfile(id) {
10010
- return getDatabase().prepare("DELETE FROM business_profiles WHERE id = ?").run(id).changes > 0;
10529
+ function rebuildInvoiceSearchIndex(db) {
10530
+ db.exec("DELETE FROM invoices_fts;");
10531
+ db.exec(`
10532
+ INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
10533
+ SELECT
10534
+ i.id,
10535
+ i.number,
10536
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
10537
+ COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
10538
+ COALESCE(i.notes, '')
10539
+ FROM invoices i;
10540
+ `);
10011
10541
  }
10012
- function rowToTaxRule(row) {
10013
- return { ...row, is_default: row.is_default === 1, reverse_charge: row.reverse_charge === 1 };
10542
+ function searchInvoices(db, query, options = {}) {
10543
+ const limit = options.limit ?? 20;
10544
+ const offset = options.offset ?? 0;
10545
+ const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
10546
+ if (options.status) {
10547
+ return db.query(`
10548
+ SELECT i.*
10549
+ FROM invoices_fts f
10550
+ JOIN invoices i ON i.id = f.invoice_id
10551
+ WHERE invoices_fts MATCH ?1
10552
+ AND i.status = ?2
10553
+ ORDER BY bm25(invoices_fts)
10554
+ LIMIT ?3
10555
+ OFFSET ?4
10556
+ `).all(escapedPhrase, options.status, limit, offset).map(mapInvoiceRow);
10557
+ }
10558
+ return db.query(`
10559
+ SELECT i.*
10560
+ FROM invoices_fts f
10561
+ JOIN invoices i ON i.id = f.invoice_id
10562
+ WHERE invoices_fts MATCH ?1
10563
+ ORDER BY bm25(invoices_fts)
10564
+ LIMIT ?2
10565
+ OFFSET ?3
10566
+ `).all(escapedPhrase, limit, offset).map(mapInvoiceRow);
10014
10567
  }
10015
- function getTaxRulesForCountry(country) {
10016
- const db = getDatabase();
10017
- return db.prepare("SELECT * FROM tax_rules WHERE country = ? ORDER BY is_default DESC, rate DESC").all(country).map(rowToTaxRule);
10568
+ // src/db/agents.ts
10569
+ import { randomUUID as randomUUID2 } from "crypto";
10570
+ function mapAgent(row) {
10571
+ return {
10572
+ id: row.id,
10573
+ name: row.name,
10574
+ description: row.description ?? undefined,
10575
+ focus: row.focus ?? undefined,
10576
+ createdAt: row.created_at,
10577
+ lastSeenAt: row.last_seen_at
10578
+ };
10018
10579
  }
10019
- function getDefaultTaxRule(country) {
10020
- const db = getDatabase();
10021
- const row = db.prepare("SELECT * FROM tax_rules WHERE country = ? AND is_default = 1").get(country);
10022
- return row ? rowToTaxRule(row) : null;
10580
+ function registerAgent(db, input) {
10581
+ const existing = db.query("SELECT * FROM agents WHERE name = ?1").get(input.name);
10582
+ if (existing) {
10583
+ db.query("UPDATE agents SET description = COALESCE(?2, description), last_seen_at = datetime('now') WHERE name = ?1").run(input.name, input.description ?? null);
10584
+ return getAgentByName(db, input.name);
10585
+ }
10586
+ const id = randomUUID2();
10587
+ db.query("INSERT INTO agents (id, name, description) VALUES (?1, ?2, ?3)").run(id, input.name, input.description ?? null);
10588
+ return getAgentById(db, id);
10023
10589
  }
10024
- function getTaxRule(id) {
10025
- const db = getDatabase();
10026
- const row = db.prepare("SELECT * FROM tax_rules WHERE id = ?").get(id);
10027
- return row ? rowToTaxRule(row) : null;
10590
+ function getAgentById(db, id) {
10591
+ const row = db.query("SELECT * FROM agents WHERE id = ?1").get(id);
10592
+ return row ? mapAgent(row) : null;
10028
10593
  }
10029
- function listAllTaxRules() {
10030
- const db = getDatabase();
10031
- return db.prepare("SELECT * FROM tax_rules ORDER BY country, is_default DESC, rate DESC").all().map(rowToTaxRule);
10594
+ function getAgentByName(db, name) {
10595
+ const row = db.query("SELECT * FROM agents WHERE name = ?1").get(name);
10596
+ return row ? mapAgent(row) : null;
10032
10597
  }
10033
- function createTaxRule(input) {
10034
- const db = getDatabase();
10035
- const id = crypto.randomUUID();
10036
- db.prepare(`INSERT INTO tax_rules (id, country, region, tax_name, rate, type, is_default, reverse_charge, description)
10037
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.country, input.region || null, input.tax_name, input.rate, input.type || "vat", input.is_default ? 1 : 0, input.reverse_charge ? 1 : 0, input.description || null);
10038
- return getTaxRule(id);
10598
+ function heartbeatAgent(db, agentId) {
10599
+ db.query("UPDATE agents SET last_seen_at = datetime('now') WHERE id = ?1").run(agentId);
10600
+ return getAgentById(db, agentId);
10039
10601
  }
10040
- function deleteTaxRule(id) {
10041
- return getDatabase().prepare("DELETE FROM tax_rules WHERE id = ?").run(id).changes > 0;
10602
+ function setAgentFocus(db, agentId, focus) {
10603
+ db.query("UPDATE agents SET focus = ?2, last_seen_at = datetime('now') WHERE id = ?1").run(agentId, focus ?? null);
10604
+ return getAgentById(db, agentId);
10042
10605
  }
10043
- function determineTax(issuerCountry, clientCountry, clientVatNumber) {
10044
- const EU_COUNTRIES = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"];
10045
- const issuerInEU = EU_COUNTRIES.includes(issuerCountry);
10046
- const clientInEU = EU_COUNTRIES.includes(clientCountry);
10047
- if (issuerInEU && clientInEU && issuerCountry !== clientCountry && clientVatNumber) {
10048
- return { tax_rate: 0, tax_name: "Reverse Charge", reverse_charge: true };
10049
- }
10050
- const defaultRule = getDefaultTaxRule(issuerCountry);
10051
- if (defaultRule) {
10052
- return { tax_rate: defaultRule.rate, tax_name: defaultRule.tax_name, reverse_charge: false };
10053
- }
10054
- return { tax_rate: 0, tax_name: "Tax", reverse_charge: false };
10606
+ function listAgents(db) {
10607
+ return db.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all().map(mapAgent);
10055
10608
  }
10056
10609
  export {
10610
+ updateParty,
10057
10611
  updateInvoiceStatus,
10058
- updateClient,
10059
- updateBusinessProfile,
10060
- removeLineItem,
10061
- recordPayment,
10612
+ submitFeedback,
10613
+ setAgentFocus,
10614
+ searchInvoices,
10615
+ registerAgent,
10616
+ rebuildInvoiceSearchIndex,
10617
+ openInvoiceDatabase,
10618
+ migratePgDatabase,
10619
+ migrateDatabase,
10620
+ listParties,
10062
10621
  listInvoices,
10063
- listClients,
10064
- listBusinessProfiles,
10065
- listAllTaxRules,
10066
- getTaxRulesForCountry,
10067
- getTaxRule,
10068
- getInvoiceWithItems,
10069
- getInvoiceSummary,
10070
- getInvoice,
10071
- getDefaultTaxRule,
10072
- getDefaultBusinessProfile,
10073
- getDatabase,
10074
- getClient,
10075
- getBusinessProfile,
10076
- determineTax,
10077
- deleteTaxRule,
10622
+ listAgents,
10623
+ heartbeatAgent,
10624
+ getPartyById,
10625
+ getInvoiceById,
10626
+ getAgentByName,
10627
+ getAgentById,
10078
10628
  deleteInvoice,
10079
- deleteClient,
10080
- deleteBusinessProfile,
10081
- createTaxRule,
10629
+ defaultPgConnectionString,
10630
+ defaultDatabasePath,
10631
+ defaultConnectionString,
10632
+ createParty,
10082
10633
  createInvoice,
10083
- createClient,
10084
- createBusinessProfile,
10085
- closeDatabase,
10086
- addLineItem
10634
+ cloudPush,
10635
+ cloudPull,
10636
+ applyMigrations,
10637
+ VERSION
10087
10638
  };