@hasna/contacts 0.2.9 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -226,6 +226,85 @@ var MIGRATIONS = [
226
226
  );
227
227
 
228
228
  CREATE INDEX IF NOT EXISTS idx_contact_notes_contact ON contact_notes(contact_id);
229
+ `,
230
+ `
231
+ ALTER TABLE companies ADD COLUMN is_owned_entity INTEGER NOT NULL DEFAULT 0;
232
+ ALTER TABLE companies ADD COLUMN entity_type TEXT CHECK(entity_type IN ('operating','holding','dissolved','nonprofit','trust','branch','other'));
233
+
234
+ ALTER TABLE company_relationships ADD COLUMN start_date TEXT;
235
+ ALTER TABLE company_relationships ADD COLUMN end_date TEXT;
236
+ ALTER TABLE company_relationships ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0;
237
+ ALTER TABLE company_relationships ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','inactive','ended'));
238
+
239
+ CREATE TABLE IF NOT EXISTS org_members (
240
+ id TEXT PRIMARY KEY,
241
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
242
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
243
+ title TEXT,
244
+ specialization TEXT,
245
+ office_phone TEXT,
246
+ response_sla_hours INTEGER,
247
+ notes TEXT,
248
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
249
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
250
+ UNIQUE(company_id, contact_id)
251
+ );
252
+
253
+ CREATE TABLE IF NOT EXISTS vendor_communications (
254
+ id TEXT PRIMARY KEY,
255
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
256
+ contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
257
+ comm_date TEXT NOT NULL,
258
+ type TEXT NOT NULL DEFAULT 'email' CHECK(type IN ('email','call','meeting','invoice_request','invoice_received','payment','dispute','other')),
259
+ direction TEXT NOT NULL DEFAULT 'outbound' CHECK(direction IN ('inbound','outbound')),
260
+ subject TEXT,
261
+ body TEXT,
262
+ status TEXT NOT NULL DEFAULT 'sent' CHECK(status IN ('sent','awaiting_response','responded','no_response','resolved')),
263
+ invoice_amount REAL,
264
+ invoice_currency TEXT,
265
+ invoice_ref TEXT,
266
+ follow_up_date TEXT,
267
+ follow_up_done INTEGER NOT NULL DEFAULT 0,
268
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
269
+ );
270
+
271
+ CREATE TABLE IF NOT EXISTS contact_tasks (
272
+ id TEXT PRIMARY KEY,
273
+ title TEXT NOT NULL,
274
+ description TEXT,
275
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
276
+ assigned_by TEXT,
277
+ deadline TEXT,
278
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','awaiting_response','in_progress','completed','cancelled','escalated')),
279
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low','medium','high','critical')),
280
+ entity_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
281
+ linked_todos_task_id TEXT,
282
+ escalation_rules TEXT NOT NULL DEFAULT '[]',
283
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
284
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
285
+ );
286
+
287
+ CREATE TABLE IF NOT EXISTS applications (
288
+ id TEXT PRIMARY KEY,
289
+ program_name TEXT NOT NULL,
290
+ provider_company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
291
+ type TEXT NOT NULL DEFAULT 'other' CHECK(type IN ('ai_credits','grant','startup_program','visa','trademark','tax_filing','loan','other')),
292
+ value_usd REAL,
293
+ applicant_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
294
+ primary_contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
295
+ status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','submitted','pending','approved','rejected','follow_up_needed','expired','cancelled')),
296
+ submitted_date TEXT,
297
+ decision_date TEXT,
298
+ follow_up_date TEXT,
299
+ notes TEXT,
300
+ method TEXT CHECK(method IN ('email','form','typeform','hubspot','manual','browser','feathery','other')),
301
+ form_url TEXT,
302
+ metadata TEXT NOT NULL DEFAULT '{}',
303
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
304
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
305
+ );
306
+
307
+ ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
229
308
  `
230
309
  ];
231
310
  var _db = null;
@@ -380,7 +459,9 @@ function rowToCompany(row) {
380
459
  ...row,
381
460
  custom_fields: JSON.parse(row.custom_fields || "{}"),
382
461
  archived: !!row.archived,
383
- project_id: row.project_id ?? null
462
+ project_id: row.project_id ?? null,
463
+ is_owned_entity: !!row.is_owned_entity,
464
+ entity_type: row.entity_type ?? null
384
465
  };
385
466
  }
386
467
  function insertEmails(db, contactId, companyId, emails) {
@@ -836,7 +917,9 @@ function rowToCompany2(row) {
836
917
  ...row,
837
918
  custom_fields: JSON.parse(row.custom_fields || "{}"),
838
919
  archived: !!row.archived,
839
- project_id: row.project_id ?? null
920
+ project_id: row.project_id ?? null,
921
+ is_owned_entity: !!row.is_owned_entity,
922
+ entity_type: row.entity_type ?? null
840
923
  };
841
924
  }
842
925
  function insertEmails2(db, companyId, emails) {
@@ -900,8 +983,8 @@ function createCompany(input, db) {
900
983
  const d = db || getDatabase();
901
984
  const id = uuid();
902
985
  const timestamp = now();
903
- d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, created_at, updated_at)
904
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
986
+ d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, is_owned_entity, entity_type, created_at, updated_at)
987
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
905
988
  id,
906
989
  input.name,
907
990
  input.domain ?? null,
@@ -912,6 +995,8 @@ function createCompany(input, db) {
912
995
  input.founded_year ?? null,
913
996
  input.notes ?? null,
914
997
  JSON.stringify(input.custom_fields ?? {}),
998
+ input.is_owned_entity ? 1 : 0,
999
+ input.entity_type ?? null,
915
1000
  timestamp,
916
1001
  timestamp
917
1002
  ]);
@@ -948,6 +1033,7 @@ function listCompanies(opts = {}, db) {
948
1033
  tag_id,
949
1034
  project_id,
950
1035
  archived = false,
1036
+ is_owned_entity,
951
1037
  order_by = "name",
952
1038
  order_dir = "asc"
953
1039
  } = opts;
@@ -967,6 +1053,10 @@ function listCompanies(opts = {}, db) {
967
1053
  conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
968
1054
  params.push(tag_id);
969
1055
  }
1056
+ if (is_owned_entity !== undefined) {
1057
+ conditions.push("co.is_owned_entity = ?");
1058
+ params.push(is_owned_entity ? 1 : 0);
1059
+ }
970
1060
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
971
1061
  const validOrderBy = ["name", "created_at", "updated_at"].includes(order_by) ? order_by : "name";
972
1062
  const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
@@ -1022,6 +1112,14 @@ function updateCompany(id, input, db) {
1022
1112
  setClauses.push("project_id = ?");
1023
1113
  params.push(input.project_id);
1024
1114
  }
1115
+ if (input.is_owned_entity !== undefined) {
1116
+ setClauses.push("is_owned_entity = ?");
1117
+ params.push(input.is_owned_entity ? 1 : 0);
1118
+ }
1119
+ if ("entity_type" in input && input.entity_type !== undefined) {
1120
+ setClauses.push("entity_type = ?");
1121
+ params.push(input.entity_type);
1122
+ }
1025
1123
  params.push(id);
1026
1124
  d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
1027
1125
  logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
@@ -1082,6 +1180,9 @@ function unarchiveCompany(id, db) {
1082
1180
  const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
1083
1181
  return loadCompanyDetails(d, rowToCompany2(updated));
1084
1182
  }
1183
+ function listOwnedEntities(db) {
1184
+ return listCompanies({ is_owned_entity: true }, db);
1185
+ }
1085
1186
  // src/db/tags.ts
1086
1187
  function rowToTag2(row) {
1087
1188
  return { ...row };
@@ -1243,6 +1344,635 @@ function deleteRelationship(id, db) {
1243
1344
  const d = db || getDatabase();
1244
1345
  d.run(`DELETE FROM contact_relationships WHERE id = ?`, [id]);
1245
1346
  }
1347
+ function rowToCompanyRelationship(row) {
1348
+ return {
1349
+ ...row,
1350
+ relationship_type: row.relationship_type,
1351
+ start_date: row.start_date ?? null,
1352
+ end_date: row.end_date ?? null,
1353
+ is_primary: !!row.is_primary,
1354
+ status: row.status ?? "active"
1355
+ };
1356
+ }
1357
+ function createCompanyRelationship(input, db) {
1358
+ const d = db || getDatabase();
1359
+ const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
1360
+ if (!contact)
1361
+ throw new ContactNotFoundError(input.contact_id);
1362
+ const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
1363
+ if (!company)
1364
+ throw new Error(`Company ${input.company_id} not found`);
1365
+ const id = uuid();
1366
+ d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1367
+ id,
1368
+ input.contact_id,
1369
+ input.company_id,
1370
+ input.relationship_type,
1371
+ input.notes ?? null,
1372
+ input.start_date ?? null,
1373
+ input.end_date ?? null,
1374
+ input.is_primary ? 1 : 0,
1375
+ input.status ?? "active"
1376
+ ]);
1377
+ return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
1378
+ }
1379
+ function listCompanyRelationships(opts = {}, db) {
1380
+ const d = db || getDatabase();
1381
+ const conditions = [];
1382
+ const params = [];
1383
+ if (opts.contact_id) {
1384
+ conditions.push("contact_id = ?");
1385
+ params.push(opts.contact_id);
1386
+ }
1387
+ if (opts.company_id) {
1388
+ conditions.push("company_id = ?");
1389
+ params.push(opts.company_id);
1390
+ }
1391
+ if (opts.relationship_type) {
1392
+ conditions.push("relationship_type = ?");
1393
+ params.push(opts.relationship_type);
1394
+ }
1395
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1396
+ const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
1397
+ return rows.map(rowToCompanyRelationship);
1398
+ }
1399
+ function deleteCompanyRelationship(id, db) {
1400
+ const d = db || getDatabase();
1401
+ d.run(`DELETE FROM company_relationships WHERE id = ?`, [id]);
1402
+ }
1403
+ function getEntityTeam(companyId, db) {
1404
+ const d = db || getDatabase();
1405
+ const rows = d.query(`SELECT id, contact_id, relationship_type, is_primary, status, start_date, end_date, notes
1406
+ FROM company_relationships WHERE company_id = ? ORDER BY is_primary DESC, created_at ASC`).all(companyId);
1407
+ return rows.map((r) => ({
1408
+ contact_id: r.contact_id,
1409
+ relationship_id: r.id,
1410
+ relationship_type: r.relationship_type,
1411
+ is_primary: !!r.is_primary,
1412
+ status: r.status,
1413
+ start_date: r.start_date,
1414
+ end_date: r.end_date,
1415
+ notes: r.notes
1416
+ }));
1417
+ }
1418
+ // src/db/org-members.ts
1419
+ function rowToOrgMember(row) {
1420
+ return {
1421
+ id: row.id,
1422
+ company_id: row.company_id,
1423
+ contact_id: row.contact_id,
1424
+ title: row.title,
1425
+ specialization: row.specialization,
1426
+ office_phone: row.office_phone,
1427
+ response_sla_hours: row.response_sla_hours,
1428
+ notes: row.notes,
1429
+ created_at: row.created_at,
1430
+ updated_at: row.updated_at
1431
+ };
1432
+ }
1433
+ function addOrgMember(input, db) {
1434
+ const d = db || getDatabase();
1435
+ const id = uuid();
1436
+ const timestamp = now();
1437
+ d.run(`INSERT INTO org_members (id, company_id, contact_id, title, specialization, office_phone, response_sla_hours, notes, created_at, updated_at)
1438
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1439
+ id,
1440
+ input.company_id,
1441
+ input.contact_id,
1442
+ input.title ?? null,
1443
+ input.specialization ?? null,
1444
+ input.office_phone ?? null,
1445
+ input.response_sla_hours ?? null,
1446
+ input.notes ?? null,
1447
+ timestamp,
1448
+ timestamp
1449
+ ]);
1450
+ return rowToOrgMember(d.query(`SELECT * FROM org_members WHERE id = ?`).get(id));
1451
+ }
1452
+ function getOrgMember(id, db) {
1453
+ const d = db || getDatabase();
1454
+ const row = d.query(`SELECT * FROM org_members WHERE id = ?`).get(id);
1455
+ return row ? rowToOrgMember(row) : null;
1456
+ }
1457
+ function listOrgMembers(companyId, db) {
1458
+ const d = db || getDatabase();
1459
+ const rows = d.query(`SELECT * FROM org_members WHERE company_id = ? ORDER BY created_at ASC`).all(companyId);
1460
+ return rows.map(rowToOrgMember);
1461
+ }
1462
+ function updateOrgMember(id, input, db) {
1463
+ const d = db || getDatabase();
1464
+ const setClauses = ["updated_at = ?"];
1465
+ const params = [now()];
1466
+ if ("title" in input) {
1467
+ setClauses.push("title = ?");
1468
+ params.push(input.title ?? null);
1469
+ }
1470
+ if ("specialization" in input) {
1471
+ setClauses.push("specialization = ?");
1472
+ params.push(input.specialization ?? null);
1473
+ }
1474
+ if ("office_phone" in input) {
1475
+ setClauses.push("office_phone = ?");
1476
+ params.push(input.office_phone ?? null);
1477
+ }
1478
+ if ("response_sla_hours" in input) {
1479
+ setClauses.push("response_sla_hours = ?");
1480
+ params.push(input.response_sla_hours ?? null);
1481
+ }
1482
+ if ("notes" in input) {
1483
+ setClauses.push("notes = ?");
1484
+ params.push(input.notes ?? null);
1485
+ }
1486
+ params.push(id);
1487
+ d.run(`UPDATE org_members SET ${setClauses.join(", ")} WHERE id = ?`, params);
1488
+ return rowToOrgMember(d.query(`SELECT * FROM org_members WHERE id = ?`).get(id));
1489
+ }
1490
+ function removeOrgMember(id, db) {
1491
+ const d = db || getDatabase();
1492
+ d.run(`DELETE FROM org_members WHERE id = ?`, [id]);
1493
+ }
1494
+ function listOrgMembersForContact(contactId, db) {
1495
+ const d = db || getDatabase();
1496
+ const rows = d.query(`SELECT * FROM org_members WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
1497
+ return rows.map(rowToOrgMember);
1498
+ }
1499
+ // src/db/vendor-comms.ts
1500
+ function rowToVendorComm(row) {
1501
+ return {
1502
+ id: row.id,
1503
+ company_id: row.company_id,
1504
+ contact_id: row.contact_id,
1505
+ comm_date: row.comm_date,
1506
+ type: row.type,
1507
+ direction: row.direction,
1508
+ subject: row.subject,
1509
+ body: row.body,
1510
+ status: row.status,
1511
+ invoice_amount: row.invoice_amount,
1512
+ invoice_currency: row.invoice_currency,
1513
+ invoice_ref: row.invoice_ref,
1514
+ follow_up_date: row.follow_up_date,
1515
+ follow_up_done: !!row.follow_up_done,
1516
+ created_at: row.created_at
1517
+ };
1518
+ }
1519
+ function logVendorCommunication(input, db) {
1520
+ const d = db || getDatabase();
1521
+ const id = uuid();
1522
+ d.run(`INSERT INTO vendor_communications
1523
+ (id, company_id, contact_id, comm_date, type, direction, subject, body, status,
1524
+ invoice_amount, invoice_currency, invoice_ref, follow_up_date, follow_up_done)
1525
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1526
+ id,
1527
+ input.company_id,
1528
+ input.contact_id ?? null,
1529
+ input.comm_date,
1530
+ input.type ?? "email",
1531
+ input.direction ?? "outbound",
1532
+ input.subject ?? null,
1533
+ input.body ?? null,
1534
+ input.status ?? "sent",
1535
+ input.invoice_amount ?? null,
1536
+ input.invoice_currency ?? null,
1537
+ input.invoice_ref ?? null,
1538
+ input.follow_up_date ?? null,
1539
+ input.follow_up_done ? 1 : 0
1540
+ ]);
1541
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
1542
+ }
1543
+ function listVendorCommunications(companyId, opts = {}, db) {
1544
+ const d = db || getDatabase();
1545
+ const conditions = ["company_id = ?"];
1546
+ const params = [companyId];
1547
+ if (opts.type) {
1548
+ conditions.push("type = ?");
1549
+ params.push(opts.type);
1550
+ }
1551
+ if (opts.status) {
1552
+ conditions.push("status = ?");
1553
+ params.push(opts.status);
1554
+ }
1555
+ if (opts.direction) {
1556
+ conditions.push("direction = ?");
1557
+ params.push(opts.direction);
1558
+ }
1559
+ const where = conditions.join(" AND ");
1560
+ const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
1561
+ return rows.map(rowToVendorComm);
1562
+ }
1563
+ function updateVendorCommunication(id, input, db) {
1564
+ const d = db || getDatabase();
1565
+ const setClauses = [];
1566
+ const params = [];
1567
+ if ("contact_id" in input) {
1568
+ setClauses.push("contact_id = ?");
1569
+ params.push(input.contact_id ?? null);
1570
+ }
1571
+ if (input.comm_date !== undefined) {
1572
+ setClauses.push("comm_date = ?");
1573
+ params.push(input.comm_date);
1574
+ }
1575
+ if (input.type !== undefined) {
1576
+ setClauses.push("type = ?");
1577
+ params.push(input.type);
1578
+ }
1579
+ if (input.direction !== undefined) {
1580
+ setClauses.push("direction = ?");
1581
+ params.push(input.direction);
1582
+ }
1583
+ if ("subject" in input) {
1584
+ setClauses.push("subject = ?");
1585
+ params.push(input.subject ?? null);
1586
+ }
1587
+ if ("body" in input) {
1588
+ setClauses.push("body = ?");
1589
+ params.push(input.body ?? null);
1590
+ }
1591
+ if (input.status !== undefined) {
1592
+ setClauses.push("status = ?");
1593
+ params.push(input.status);
1594
+ }
1595
+ if ("invoice_amount" in input) {
1596
+ setClauses.push("invoice_amount = ?");
1597
+ params.push(input.invoice_amount ?? null);
1598
+ }
1599
+ if ("invoice_currency" in input) {
1600
+ setClauses.push("invoice_currency = ?");
1601
+ params.push(input.invoice_currency ?? null);
1602
+ }
1603
+ if ("invoice_ref" in input) {
1604
+ setClauses.push("invoice_ref = ?");
1605
+ params.push(input.invoice_ref ?? null);
1606
+ }
1607
+ if ("follow_up_date" in input) {
1608
+ setClauses.push("follow_up_date = ?");
1609
+ params.push(input.follow_up_date ?? null);
1610
+ }
1611
+ if (input.follow_up_done !== undefined) {
1612
+ setClauses.push("follow_up_done = ?");
1613
+ params.push(input.follow_up_done ? 1 : 0);
1614
+ }
1615
+ if (setClauses.length > 0) {
1616
+ params.push(id);
1617
+ d.run(`UPDATE vendor_communications SET ${setClauses.join(", ")} WHERE id = ?`, params);
1618
+ }
1619
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
1620
+ }
1621
+ function deleteVendorCommunication(id, db) {
1622
+ const d = db || getDatabase();
1623
+ d.run(`DELETE FROM vendor_communications WHERE id = ?`, [id]);
1624
+ }
1625
+ function listPendingFollowUps(db) {
1626
+ const d = db || getDatabase();
1627
+ const today = new Date().toISOString().slice(0, 10);
1628
+ const rows = d.query(`SELECT * FROM vendor_communications
1629
+ WHERE follow_up_date <= ? AND follow_up_done = 0
1630
+ ORDER BY follow_up_date ASC`).all(today);
1631
+ return rows.map(rowToVendorComm);
1632
+ }
1633
+ function listMissingInvoices(db) {
1634
+ const d = db || getDatabase();
1635
+ const rows = d.query(`SELECT * FROM vendor_communications
1636
+ WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
1637
+ ORDER BY comm_date ASC`).all();
1638
+ return rows.map(rowToVendorComm);
1639
+ }
1640
+ function markFollowUpDone(id, db) {
1641
+ const d = db || getDatabase();
1642
+ d.run(`UPDATE vendor_communications SET follow_up_done = 1 WHERE id = ?`, [id]);
1643
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
1644
+ }
1645
+ // src/db/contact-tasks.ts
1646
+ function rowToContactTask(row) {
1647
+ return {
1648
+ id: row.id,
1649
+ title: row.title,
1650
+ description: row.description,
1651
+ contact_id: row.contact_id,
1652
+ assigned_by: row.assigned_by,
1653
+ deadline: row.deadline,
1654
+ status: row.status,
1655
+ priority: row.priority,
1656
+ entity_id: row.entity_id,
1657
+ linked_todos_task_id: row.linked_todos_task_id,
1658
+ escalation_rules: JSON.parse(row.escalation_rules || "[]"),
1659
+ created_at: row.created_at,
1660
+ updated_at: row.updated_at
1661
+ };
1662
+ }
1663
+ function createContactTask(input, db) {
1664
+ const d = db || getDatabase();
1665
+ const id = uuid();
1666
+ const timestamp = now();
1667
+ d.run(`INSERT INTO contact_tasks
1668
+ (id, title, description, contact_id, assigned_by, deadline, status, priority,
1669
+ entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
1670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1671
+ id,
1672
+ input.title,
1673
+ input.description ?? null,
1674
+ input.contact_id,
1675
+ input.assigned_by ?? null,
1676
+ input.deadline ?? null,
1677
+ input.status ?? "pending",
1678
+ input.priority ?? "medium",
1679
+ input.entity_id ?? null,
1680
+ input.linked_todos_task_id ?? null,
1681
+ JSON.stringify(input.escalation_rules ?? []),
1682
+ timestamp,
1683
+ timestamp
1684
+ ]);
1685
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
1686
+ }
1687
+ function getContactTask(id, db) {
1688
+ const d = db || getDatabase();
1689
+ const row = d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id);
1690
+ return row ? rowToContactTask(row) : null;
1691
+ }
1692
+ function listContactTasks(opts = {}, db) {
1693
+ const d = db || getDatabase();
1694
+ const conditions = [];
1695
+ const params = [];
1696
+ if (opts.contact_id) {
1697
+ conditions.push("contact_id = ?");
1698
+ params.push(opts.contact_id);
1699
+ }
1700
+ if (opts.entity_id) {
1701
+ conditions.push("entity_id = ?");
1702
+ params.push(opts.entity_id);
1703
+ }
1704
+ if (opts.status) {
1705
+ conditions.push("status = ?");
1706
+ params.push(opts.status);
1707
+ }
1708
+ if (opts.priority) {
1709
+ conditions.push("priority = ?");
1710
+ params.push(opts.priority);
1711
+ }
1712
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1713
+ const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
1714
+ return rows.map(rowToContactTask);
1715
+ }
1716
+ function updateContactTask(id, input, db) {
1717
+ const d = db || getDatabase();
1718
+ const setClauses = ["updated_at = ?"];
1719
+ const params = [now()];
1720
+ if (input.title !== undefined) {
1721
+ setClauses.push("title = ?");
1722
+ params.push(input.title);
1723
+ }
1724
+ if ("description" in input) {
1725
+ setClauses.push("description = ?");
1726
+ params.push(input.description ?? null);
1727
+ }
1728
+ if ("assigned_by" in input) {
1729
+ setClauses.push("assigned_by = ?");
1730
+ params.push(input.assigned_by ?? null);
1731
+ }
1732
+ if ("deadline" in input) {
1733
+ setClauses.push("deadline = ?");
1734
+ params.push(input.deadline ?? null);
1735
+ }
1736
+ if (input.status !== undefined) {
1737
+ setClauses.push("status = ?");
1738
+ params.push(input.status);
1739
+ }
1740
+ if (input.priority !== undefined) {
1741
+ setClauses.push("priority = ?");
1742
+ params.push(input.priority);
1743
+ }
1744
+ if ("entity_id" in input) {
1745
+ setClauses.push("entity_id = ?");
1746
+ params.push(input.entity_id ?? null);
1747
+ }
1748
+ if ("linked_todos_task_id" in input) {
1749
+ setClauses.push("linked_todos_task_id = ?");
1750
+ params.push(input.linked_todos_task_id ?? null);
1751
+ }
1752
+ if (input.escalation_rules !== undefined) {
1753
+ setClauses.push("escalation_rules = ?");
1754
+ params.push(JSON.stringify(input.escalation_rules));
1755
+ }
1756
+ params.push(id);
1757
+ d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
1758
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
1759
+ }
1760
+ function deleteContactTask(id, db) {
1761
+ const d = db || getDatabase();
1762
+ d.run(`DELETE FROM contact_tasks WHERE id = ?`, [id]);
1763
+ }
1764
+ function listOverdueTasks(db) {
1765
+ const d = db || getDatabase();
1766
+ const now_iso = new Date().toISOString();
1767
+ const rows = d.query(`SELECT * FROM contact_tasks
1768
+ WHERE deadline < ? AND status NOT IN ('completed','cancelled')
1769
+ ORDER BY deadline ASC`).all(now_iso);
1770
+ return rows.map(rowToContactTask);
1771
+ }
1772
+ function checkEscalations(db) {
1773
+ const overdue = listOverdueTasks(db);
1774
+ const now_ms = Date.now();
1775
+ const results = [];
1776
+ for (const task of overdue) {
1777
+ if (!task.deadline || task.escalation_rules.length === 0)
1778
+ continue;
1779
+ const deadlineMs = new Date(task.deadline).getTime();
1780
+ const daysPastDeadline = (now_ms - deadlineMs) / (1000 * 60 * 60 * 24);
1781
+ const triggered = task.escalation_rules.filter((rule) => daysPastDeadline >= rule.after_days);
1782
+ if (triggered.length > 0) {
1783
+ results.push({ task, rules_triggered: triggered });
1784
+ }
1785
+ }
1786
+ return results;
1787
+ }
1788
+ // src/db/applications.ts
1789
+ function rowToApplication(row) {
1790
+ return {
1791
+ id: row.id,
1792
+ program_name: row.program_name,
1793
+ provider_company_id: row.provider_company_id,
1794
+ type: row.type,
1795
+ value_usd: row.value_usd,
1796
+ applicant_contact_id: row.applicant_contact_id,
1797
+ primary_contact_id: row.primary_contact_id,
1798
+ status: row.status,
1799
+ submitted_date: row.submitted_date,
1800
+ decision_date: row.decision_date,
1801
+ follow_up_date: row.follow_up_date,
1802
+ notes: row.notes,
1803
+ method: row.method ?? null,
1804
+ form_url: row.form_url,
1805
+ metadata: JSON.parse(row.metadata || "{}"),
1806
+ created_at: row.created_at,
1807
+ updated_at: row.updated_at
1808
+ };
1809
+ }
1810
+ function createApplication(input, db) {
1811
+ const d = db || getDatabase();
1812
+ const id = uuid();
1813
+ const timestamp = now();
1814
+ d.run(`INSERT INTO applications
1815
+ (id, program_name, provider_company_id, type, value_usd, applicant_contact_id, primary_contact_id,
1816
+ status, submitted_date, decision_date, follow_up_date, notes, method, form_url, metadata, created_at, updated_at)
1817
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1818
+ id,
1819
+ input.program_name,
1820
+ input.provider_company_id ?? null,
1821
+ input.type ?? "other",
1822
+ input.value_usd ?? null,
1823
+ input.applicant_contact_id ?? null,
1824
+ input.primary_contact_id ?? null,
1825
+ input.status ?? "draft",
1826
+ input.submitted_date ?? null,
1827
+ input.decision_date ?? null,
1828
+ input.follow_up_date ?? null,
1829
+ input.notes ?? null,
1830
+ input.method ?? null,
1831
+ input.form_url ?? null,
1832
+ JSON.stringify(input.metadata ?? {}),
1833
+ timestamp,
1834
+ timestamp
1835
+ ]);
1836
+ return rowToApplication(d.query(`SELECT * FROM applications WHERE id = ?`).get(id));
1837
+ }
1838
+ function getApplication(id, db) {
1839
+ const d = db || getDatabase();
1840
+ const row = d.query(`SELECT * FROM applications WHERE id = ?`).get(id);
1841
+ return row ? rowToApplication(row) : null;
1842
+ }
1843
+ function listApplications(opts = {}, db) {
1844
+ const d = db || getDatabase();
1845
+ const conditions = [];
1846
+ const params = [];
1847
+ if (opts.type) {
1848
+ conditions.push("type = ?");
1849
+ params.push(opts.type);
1850
+ }
1851
+ if (opts.status) {
1852
+ conditions.push("status = ?");
1853
+ params.push(opts.status);
1854
+ }
1855
+ if (opts.provider_company_id) {
1856
+ conditions.push("provider_company_id = ?");
1857
+ params.push(opts.provider_company_id);
1858
+ }
1859
+ if (opts.applicant_contact_id) {
1860
+ conditions.push("applicant_contact_id = ?");
1861
+ params.push(opts.applicant_contact_id);
1862
+ }
1863
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1864
+ const rows = d.query(`SELECT * FROM applications ${where} ORDER BY created_at DESC`).all(...params);
1865
+ return rows.map(rowToApplication);
1866
+ }
1867
+ function updateApplication(id, input, db) {
1868
+ const d = db || getDatabase();
1869
+ const setClauses = ["updated_at = ?"];
1870
+ const params = [now()];
1871
+ if (input.program_name !== undefined) {
1872
+ setClauses.push("program_name = ?");
1873
+ params.push(input.program_name);
1874
+ }
1875
+ if ("provider_company_id" in input) {
1876
+ setClauses.push("provider_company_id = ?");
1877
+ params.push(input.provider_company_id ?? null);
1878
+ }
1879
+ if (input.type !== undefined) {
1880
+ setClauses.push("type = ?");
1881
+ params.push(input.type);
1882
+ }
1883
+ if ("value_usd" in input) {
1884
+ setClauses.push("value_usd = ?");
1885
+ params.push(input.value_usd ?? null);
1886
+ }
1887
+ if ("applicant_contact_id" in input) {
1888
+ setClauses.push("applicant_contact_id = ?");
1889
+ params.push(input.applicant_contact_id ?? null);
1890
+ }
1891
+ if ("primary_contact_id" in input) {
1892
+ setClauses.push("primary_contact_id = ?");
1893
+ params.push(input.primary_contact_id ?? null);
1894
+ }
1895
+ if (input.status !== undefined) {
1896
+ setClauses.push("status = ?");
1897
+ params.push(input.status);
1898
+ }
1899
+ if ("submitted_date" in input) {
1900
+ setClauses.push("submitted_date = ?");
1901
+ params.push(input.submitted_date ?? null);
1902
+ }
1903
+ if ("decision_date" in input) {
1904
+ setClauses.push("decision_date = ?");
1905
+ params.push(input.decision_date ?? null);
1906
+ }
1907
+ if ("follow_up_date" in input) {
1908
+ setClauses.push("follow_up_date = ?");
1909
+ params.push(input.follow_up_date ?? null);
1910
+ }
1911
+ if ("notes" in input) {
1912
+ setClauses.push("notes = ?");
1913
+ params.push(input.notes ?? null);
1914
+ }
1915
+ if ("method" in input) {
1916
+ setClauses.push("method = ?");
1917
+ params.push(input.method ?? null);
1918
+ }
1919
+ if ("form_url" in input) {
1920
+ setClauses.push("form_url = ?");
1921
+ params.push(input.form_url ?? null);
1922
+ }
1923
+ if (input.metadata !== undefined) {
1924
+ setClauses.push("metadata = ?");
1925
+ params.push(JSON.stringify(input.metadata));
1926
+ }
1927
+ params.push(id);
1928
+ d.run(`UPDATE applications SET ${setClauses.join(", ")} WHERE id = ?`, params);
1929
+ return rowToApplication(d.query(`SELECT * FROM applications WHERE id = ?`).get(id));
1930
+ }
1931
+ function deleteApplication(id, db) {
1932
+ const d = db || getDatabase();
1933
+ d.run(`DELETE FROM applications WHERE id = ?`, [id]);
1934
+ }
1935
+ function listFollowUpDue(db) {
1936
+ const d = db || getDatabase();
1937
+ const today = new Date().toISOString().slice(0, 10);
1938
+ const rows = d.query(`SELECT * FROM applications
1939
+ WHERE follow_up_date <= ? AND status = 'follow_up_needed'
1940
+ ORDER BY follow_up_date ASC`).all(today);
1941
+ return rows.map(rowToApplication);
1942
+ }
1943
+ function listPendingApplications(db) {
1944
+ const d = db || getDatabase();
1945
+ const rows = d.query(`SELECT * FROM applications
1946
+ WHERE status IN ('draft','submitted','pending')
1947
+ ORDER BY created_at DESC`).all();
1948
+ return rows.map(rowToApplication);
1949
+ }
1950
+ // src/db/notes.ts
1951
+ function addNote(contactId, body, createdBy, db, companyId) {
1952
+ const d = db || getDatabase();
1953
+ const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
1954
+ if (!contact)
1955
+ throw new ContactNotFoundError(contactId);
1956
+ const id = uuid();
1957
+ d.run(`INSERT INTO contact_notes (id, contact_id, body, created_by, company_id) VALUES (?, ?, ?, ?, ?)`, [id, contactId, body, createdBy ?? null, companyId ?? null]);
1958
+ return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(id);
1959
+ }
1960
+ function listNotes(contactId, db) {
1961
+ const d = db || getDatabase();
1962
+ return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
1963
+ }
1964
+ function listNotesForContactAtCompany(contactId, companyId, db) {
1965
+ const d = db || getDatabase();
1966
+ return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? AND company_id = ? ORDER BY created_at ASC`).all(contactId, companyId);
1967
+ }
1968
+ function deleteNote(noteId, db) {
1969
+ const d = db || getDatabase();
1970
+ d.run(`DELETE FROM contact_notes WHERE id = ?`, [noteId]);
1971
+ }
1972
+ function getNote(noteId, db) {
1973
+ const d = db || getDatabase();
1974
+ return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(noteId);
1975
+ }
1246
1976
  // src/db/groups.ts
1247
1977
  function createGroup(db, input) {
1248
1978
  const id = uuid();
@@ -1661,10 +2391,14 @@ async function pullGoogleContactsAsInputs(opts = {}) {
1661
2391
  return people.filter((p) => p.emailAddresses?.some((e) => e.value)).map(googlePersonToContactInput);
1662
2392
  }
1663
2393
  export {
2394
+ updateVendorCommunication,
1664
2395
  updateTag,
2396
+ updateOrgMember,
1665
2397
  updateGroup,
2398
+ updateContactTask,
1666
2399
  updateContact,
1667
2400
  updateCompany,
2401
+ updateApplication,
1668
2402
  unarchiveContact,
1669
2403
  unarchiveCompany,
1670
2404
  searchGoogleContacts,
@@ -1674,6 +2408,7 @@ export {
1674
2408
  resetDatabase,
1675
2409
  removeTagFromContact,
1676
2410
  removeTagFromCompany,
2411
+ removeOrgMember,
1677
2412
  removeContactFromGroup,
1678
2413
  removeCompanyFromGroup,
1679
2414
  readConnectorTokens,
@@ -1682,51 +2417,83 @@ export {
1682
2417
  parseName,
1683
2418
  parseAddressHeader,
1684
2419
  mergeContacts,
2420
+ markFollowUpDone,
2421
+ logVendorCommunication,
1685
2422
  logActivity,
2423
+ listVendorCommunications,
1686
2424
  listTags,
1687
2425
  listRelationships,
1688
2426
  listRecentContacts,
2427
+ listPendingFollowUps,
2428
+ listPendingApplications,
2429
+ listOwnedEntities,
2430
+ listOverdueTasks,
2431
+ listOrgMembersForContact,
2432
+ listOrgMembers,
2433
+ listNotesForContactAtCompany,
2434
+ listNotes,
2435
+ listMissingInvoices,
1689
2436
  listGroupsForContact,
1690
2437
  listGroupsForCompany,
1691
2438
  listGroups,
1692
2439
  listGoogleContacts,
2440
+ listFollowUpDue,
1693
2441
  listContactsInGroup,
1694
2442
  listContactsByTag,
1695
2443
  listContacts,
2444
+ listContactTasks,
2445
+ listCompanyRelationships,
1696
2446
  listCompanyEmployees,
1697
2447
  listCompaniesInGroup,
1698
2448
  listCompanies,
2449
+ listApplications,
1699
2450
  listActivity,
1700
2451
  googlePersonToContactInput,
1701
2452
  getTagByName,
1702
2453
  getTag,
1703
2454
  getRelationship,
2455
+ getOrgMember,
2456
+ getNote,
1704
2457
  getGroup,
2458
+ getEntityTeam,
1705
2459
  getDatabase,
2460
+ getContactTask,
1706
2461
  getContactByEmail,
1707
2462
  getContact,
1708
2463
  getConnectorTokenPath,
1709
2464
  getCompany,
2465
+ getApplication,
1710
2466
  getActivity,
1711
2467
  extractContactsFromGmail,
1712
2468
  domainToCompany,
2469
+ deleteVendorCommunication,
1713
2470
  deleteTag,
1714
2471
  deleteRelationship,
2472
+ deleteNote,
1715
2473
  deleteGroup,
2474
+ deleteContactTask,
1716
2475
  deleteContact,
2476
+ deleteCompanyRelationship,
1717
2477
  deleteCompany,
2478
+ deleteApplication,
1718
2479
  createTag,
1719
2480
  createRelationship,
1720
2481
  createGroup,
2482
+ createContactTask,
1721
2483
  createContact,
2484
+ createCompanyRelationship,
1722
2485
  createCompany,
2486
+ createApplication,
1723
2487
  contactToGoogleArgs,
2488
+ checkEscalations,
1724
2489
  autoLinkContactToCompany,
1725
2490
  archiveContact,
1726
2491
  archiveCompany,
1727
2492
  addTagToContact,
1728
2493
  addTagToCompany,
1729
2494
  addPhoneToContact,
2495
+ addOrgMember,
2496
+ addNote,
1730
2497
  addEmailToContact,
1731
2498
  addContactToGroup,
1732
2499
  addCompanyToGroup,