@talkpilot/core-db 1.3.71 → 1.3.72

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 (33) hide show
  1. package/README.md +1 -0
  2. package/dist/municipal/muniIssues/index.d.ts +2 -2
  3. package/dist/municipal/muniIssues/index.d.ts.map +1 -1
  4. package/dist/municipal/muniIssues/index.js +2 -1
  5. package/dist/municipal/muniIssues/index.js.map +1 -1
  6. package/dist/municipal/muniIssues/muniIssues.constants.d.ts +2 -0
  7. package/dist/municipal/muniIssues/muniIssues.constants.d.ts.map +1 -1
  8. package/dist/municipal/muniIssues/muniIssues.constants.js +8 -1
  9. package/dist/municipal/muniIssues/muniIssues.constants.js.map +1 -1
  10. package/dist/municipal/muniIssues/muniIssues.getters.d.ts +6 -1
  11. package/dist/municipal/muniIssues/muniIssues.getters.d.ts.map +1 -1
  12. package/dist/municipal/muniIssues/muniIssues.getters.js +31 -1
  13. package/dist/municipal/muniIssues/muniIssues.getters.js.map +1 -1
  14. package/dist/municipal/muniIssues/muniIssues.schema.d.ts.map +1 -1
  15. package/dist/municipal/muniIssues/muniIssues.schema.js +13 -0
  16. package/dist/municipal/muniIssues/muniIssues.schema.js.map +1 -1
  17. package/dist/municipal/muniIssues/muniIssues.setters.d.ts +5 -1
  18. package/dist/municipal/muniIssues/muniIssues.setters.d.ts.map +1 -1
  19. package/dist/municipal/muniIssues/muniIssues.setters.js +39 -2
  20. package/dist/municipal/muniIssues/muniIssues.setters.js.map +1 -1
  21. package/dist/municipal/muniIssues/muniIssues.types.d.ts +23 -2
  22. package/dist/municipal/muniIssues/muniIssues.types.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/municipal/muniIssues/__tests__/muniIssues.getters.spec.ts +60 -0
  25. package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +74 -1
  26. package/src/municipal/muniIssues/__tests__/muniIssues.setup.spec.ts +1 -0
  27. package/src/municipal/muniIssues/index.ts +5 -0
  28. package/src/municipal/muniIssues/muniIssues.constants.ts +8 -0
  29. package/src/municipal/muniIssues/muniIssues.getters.ts +46 -2
  30. package/src/municipal/muniIssues/muniIssues.schema.ts +14 -0
  31. package/src/municipal/muniIssues/muniIssues.setters.ts +59 -3
  32. package/src/municipal/muniIssues/muniIssues.types.ts +34 -4
  33. package/src/talkpilot/calls/__tests__/calls.spec.ts +519 -519
package/README.md CHANGED
@@ -183,6 +183,7 @@ Refer to `DEVELOPMENT.md` for the full walkthrough, token instructions, and fact
183
183
  | 1.3.62 | Pause/Resume support for WebsiteTalk scans (CP-1752): `PAUSED` and `CANCELED` added to `SCAN_STATUSES` (`PAUSED` deliberately excluded from `FINISHED_SCAN_STATUSES` — pausing isn't done, `CANCELED` is). Added `Scan.pausedAt`, tracked across pause/resume so elapsed-time display can freeze correctly instead of counting the paused duration. |
184
184
  | 1.3.64 | New `callTokenUsage` collection with `incrementCallTokenUsage` (CP-1810): per-call realtime token usage, one document per `callSid`. |
185
185
  | 1.3.71 | Who ended a call (CP-1873): optional `Call.endedBy` (`system` / `agent` / `user`), `CallEndedByValues`, and `setCallEndedBy` (first write wins). **Breaking** vs 1.3.69–1.3.70: the value `resident` was replaced by `user`. |
186
+ | 1.3.72 | Complaint tracking on `muniIssues` (CP-1888): optional `status` / `response` / `updatedAt`, `updateMuniIssueStatus`, `setMuniIssueResponse` (one response, then locked), `findMuniIssuesByClient`, `countMuniIssuesByClient`, `getMuniIssueByIdForClient`, index `clientId_1_createdAt_-1`. |
186
187
 
187
188
  ### 1.1.9
188
189
 
@@ -1,7 +1,7 @@
1
1
  export * from "./muniIssues.getters";
2
2
  export * from "./muniIssues.setters";
3
3
  export * from "./muniIssues.setup";
4
- export { MUNI_ISSUES_COLLECTION, MUNI_ISSUE_SYNC_STATUSES, MUNI_ISSUE_TEXT_MAX_LENGTH, MUNI_ISSUE_NAME_MAX_LENGTH, } from "./muniIssues.constants";
4
+ export { MUNI_ISSUES_COLLECTION, MUNI_ISSUE_STATUSES, MUNI_ISSUE_SYNC_STATUSES, MUNI_ISSUE_TEXT_MAX_LENGTH, MUNI_ISSUE_NAME_MAX_LENGTH, } from "./muniIssues.constants";
5
5
  export { muniIssuesJsonSchema, muniIssuesIndexes } from "./muniIssues.schema";
6
- export type { MuniIssue, MuniIssueContent, MuniIssueJira, MuniIssueSyncStatus, CreateMuniIssueInput, } from "./muniIssues.types";
6
+ export type { MuniIssue, MuniIssueContent, MuniIssueJira, MuniIssueSyncStatus, MuniIssueStatus, MuniIssueResponse, CreateMuniIssueInput, SetMuniIssueResponseInput, FindMuniIssuesOptions, } from "./muniIssues.types";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,YAAY,EACV,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -14,12 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.muniIssuesIndexes = exports.muniIssuesJsonSchema = exports.MUNI_ISSUE_NAME_MAX_LENGTH = exports.MUNI_ISSUE_TEXT_MAX_LENGTH = exports.MUNI_ISSUE_SYNC_STATUSES = exports.MUNI_ISSUES_COLLECTION = void 0;
17
+ exports.muniIssuesIndexes = exports.muniIssuesJsonSchema = exports.MUNI_ISSUE_NAME_MAX_LENGTH = exports.MUNI_ISSUE_TEXT_MAX_LENGTH = exports.MUNI_ISSUE_SYNC_STATUSES = exports.MUNI_ISSUE_STATUSES = exports.MUNI_ISSUES_COLLECTION = void 0;
18
18
  __exportStar(require("./muniIssues.getters"), exports);
19
19
  __exportStar(require("./muniIssues.setters"), exports);
20
20
  __exportStar(require("./muniIssues.setup"), exports);
21
21
  var muniIssues_constants_1 = require("./muniIssues.constants");
22
22
  Object.defineProperty(exports, "MUNI_ISSUES_COLLECTION", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUES_COLLECTION; } });
23
+ Object.defineProperty(exports, "MUNI_ISSUE_STATUSES", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_STATUSES; } });
23
24
  Object.defineProperty(exports, "MUNI_ISSUE_SYNC_STATUSES", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_SYNC_STATUSES; } });
24
25
  Object.defineProperty(exports, "MUNI_ISSUE_TEXT_MAX_LENGTH", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_TEXT_MAX_LENGTH; } });
25
26
  Object.defineProperty(exports, "MUNI_ISSUE_NAME_MAX_LENGTH", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_NAME_MAX_LENGTH; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,uDAAqC;AACrC,qDAAmC;AACnC,+DAKgC;AAJ9B,8HAAA,sBAAsB,OAAA;AACtB,gIAAA,wBAAwB,OAAA;AACxB,kIAAA,0BAA0B,OAAA;AAC1B,kIAAA,0BAA0B,OAAA;AAE5B,yDAA8E;AAArE,yHAAA,oBAAoB,OAAA;AAAE,sHAAA,iBAAiB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,uDAAqC;AACrC,qDAAmC;AACnC,+DAMgC;AAL9B,8HAAA,sBAAsB,OAAA;AACtB,2HAAA,mBAAmB,OAAA;AACnB,gIAAA,wBAAwB,OAAA;AACxB,kIAAA,0BAA0B,OAAA;AAC1B,kIAAA,0BAA0B,OAAA;AAE5B,yDAA8E;AAArE,yHAAA,oBAAoB,OAAA;AAAE,sHAAA,iBAAiB,OAAA"}
@@ -2,6 +2,8 @@
2
2
  export declare const MUNI_ISSUES_COLLECTION = "muniIssues";
3
3
  /** Sync state of the issue's Jira ticket at write time. */
4
4
  export declare const MUNI_ISSUE_SYNC_STATUSES: readonly ["created", "failed"];
5
+ /** Customer-facing complaint status, set manually by the TalkPilot team. */
6
+ export declare const MUNI_ISSUE_STATUSES: readonly ["in_review", "in_progress", "resolved", "no_issue_found"];
5
7
  /** Max length for the long free-text fields — well above the ~2000-char target. */
6
8
  export declare const MUNI_ISSUE_TEXT_MAX_LENGTH = 5000;
7
9
  /** Max length for the short identity fields (a person's name and role). */
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.constants.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.constants.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,gCAAiC,CAAC;AAEvE,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,2EAA2E;AAC3E,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,uGAAuG;AACvG,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,EAAG,QAAiB,CAAC;AAClD,eAAO,MAAM,iBAAiB,EAAG,OAAgB,CAAC"}
1
+ {"version":3,"file":"muniIssues.constants.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.constants.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAEnD,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,gCAAiC,CAAC;AAEvE,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,qEAKtB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,2EAA2E;AAC3E,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,uGAAuG;AACvG,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,EAAG,QAAiB,CAAC;AAClD,eAAO,MAAM,iBAAiB,EAAG,OAAgB,CAAC"}
@@ -1,10 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VALIDATION_ACTION = exports.VALIDATION_LEVEL = exports.NAMESPACE_EXISTS = exports.MUNI_ISSUE_NAME_MAX_LENGTH = exports.MUNI_ISSUE_TEXT_MAX_LENGTH = exports.MUNI_ISSUE_SYNC_STATUSES = exports.MUNI_ISSUES_COLLECTION = void 0;
3
+ exports.VALIDATION_ACTION = exports.VALIDATION_LEVEL = exports.NAMESPACE_EXISTS = exports.MUNI_ISSUE_NAME_MAX_LENGTH = exports.MUNI_ISSUE_TEXT_MAX_LENGTH = exports.MUNI_ISSUE_STATUSES = exports.MUNI_ISSUE_SYNC_STATUSES = exports.MUNI_ISSUES_COLLECTION = void 0;
4
4
  /** Mongo collection name for reported municipal issues. */
5
5
  exports.MUNI_ISSUES_COLLECTION = "muniIssues";
6
6
  /** Sync state of the issue's Jira ticket at write time. */
7
7
  exports.MUNI_ISSUE_SYNC_STATUSES = ["created", "failed"];
8
+ /** Customer-facing complaint status, set manually by the TalkPilot team. */
9
+ exports.MUNI_ISSUE_STATUSES = [
10
+ "in_review",
11
+ "in_progress",
12
+ "resolved",
13
+ "no_issue_found",
14
+ ];
8
15
  /** Max length for the long free-text fields — well above the ~2000-char target. */
9
16
  exports.MUNI_ISSUE_TEXT_MAX_LENGTH = 5000;
10
17
  /** Max length for the short identity fields (a person's name and role). */
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.constants.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.constants.ts"],"names":[],"mappings":";;;AAAA,2DAA2D;AAC9C,QAAA,sBAAsB,GAAG,YAAY,CAAC;AAEnD,2DAA2D;AAC9C,QAAA,wBAAwB,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAU,CAAC;AAEvE,mFAAmF;AACtE,QAAA,0BAA0B,GAAG,IAAI,CAAC;AAE/C,2EAA2E;AAC9D,QAAA,0BAA0B,GAAG,GAAG,CAAC;AAE9C,uGAAuG;AAC1F,QAAA,gBAAgB,GAAG,EAAE,CAAC;AAEnC,yEAAyE;AAC5D,QAAA,gBAAgB,GAAG,QAAiB,CAAC;AACrC,QAAA,iBAAiB,GAAG,OAAgB,CAAC"}
1
+ {"version":3,"file":"muniIssues.constants.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.constants.ts"],"names":[],"mappings":";;;AAAA,2DAA2D;AAC9C,QAAA,sBAAsB,GAAG,YAAY,CAAC;AAEnD,2DAA2D;AAC9C,QAAA,wBAAwB,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAU,CAAC;AAEvE,4EAA4E;AAC/D,QAAA,mBAAmB,GAAG;IACjC,WAAW;IACX,aAAa;IACb,UAAU;IACV,gBAAgB;CACR,CAAC;AAEX,mFAAmF;AACtE,QAAA,0BAA0B,GAAG,IAAI,CAAC;AAE/C,2EAA2E;AAC9D,QAAA,0BAA0B,GAAG,GAAG,CAAC;AAE9C,uGAAuG;AAC1F,QAAA,gBAAgB,GAAG,EAAE,CAAC;AAEnC,yEAAyE;AAC5D,QAAA,gBAAgB,GAAG,QAAiB,CAAC;AACrC,QAAA,iBAAiB,GAAG,OAAgB,CAAC"}
@@ -1,7 +1,12 @@
1
1
  import { Collection, ObjectId } from "mongodb";
2
- import { MuniIssue } from "./muniIssues.types";
2
+ import { FindMuniIssuesOptions, MuniIssue } from "./muniIssues.types";
3
3
  export declare const getMuniIssuesCollection: () => Collection<MuniIssue>;
4
4
  export declare const generateMuniIssueId: () => ObjectId;
5
5
  export declare const getMuniIssueById: (id: string) => Promise<MuniIssue | null>;
6
6
  export declare const getMuniIssueByJiraKey: (jiraKey: string) => Promise<MuniIssue | null>;
7
+ /** A client's tracked issues, newest first. */
8
+ export declare const findMuniIssuesByClient: (clientId: string, { answered, skip, limit }?: FindMuniIssuesOptions) => Promise<MuniIssue[]>;
9
+ export declare const countMuniIssuesByClient: (clientId: string, { answered }?: Pick<FindMuniIssuesOptions, "answered">) => Promise<number>;
10
+ /** One tracked issue, only if it belongs to the client. */
11
+ export declare const getMuniIssueByIdForClient: (id: string, clientId: string) => Promise<MuniIssue | null>;
7
12
  //# sourceMappingURL=muniIssues.getters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.getters.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.getters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG/C,eAAO,MAAM,uBAAuB,QAAO,UAAU,CAAC,SAAS,CAE9D,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAO,QAA0B,CAAC;AAElE,eAAO,MAAM,gBAAgB,GAC3B,IAAI,MAAM,KACT,OAAO,CAAC,SAAS,GAAG,IAAI,CAI1B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,KACd,OAAO,CAAC,SAAS,GAAG,IAAI,CAE1B,CAAC"}
1
+ {"version":3,"file":"muniIssues.getters.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.getters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAU,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGtE,eAAO,MAAM,uBAAuB,QAAO,UAAU,CAAC,SAAS,CAE9D,CAAC;AAEF,eAAO,MAAM,mBAAmB,QAAO,QAA0B,CAAC;AAElE,eAAO,MAAM,gBAAgB,GAC3B,IAAI,MAAM,KACT,OAAO,CAAC,SAAS,GAAG,IAAI,CAI1B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,KACd,OAAO,CAAC,SAAS,GAAG,IAAI,CAE1B,CAAC;AAYF,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,GACjC,UAAU,MAAM,EAChB,4BAAmC,qBAA0B,KAC5D,OAAO,CAAC,SAAS,EAAE,CAOrB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,UAAU,MAAM,EAChB,eAAc,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAM,KACzD,OAAO,CAAC,MAAM,CAIhB,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,yBAAyB,GACpC,IAAI,MAAM,EACV,UAAU,MAAM,KACf,OAAO,CAAC,SAAS,GAAG,IAAI,CAM1B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getMuniIssueByJiraKey = exports.getMuniIssueById = exports.generateMuniIssueId = exports.getMuniIssuesCollection = void 0;
3
+ exports.getMuniIssueByIdForClient = exports.countMuniIssuesByClient = exports.findMuniIssuesByClient = exports.getMuniIssueByJiraKey = exports.getMuniIssueById = exports.generateMuniIssueId = exports.getMuniIssuesCollection = void 0;
4
4
  const mongodb_1 = require("mongodb");
5
5
  const index_1 = require("../index");
6
6
  const muniIssues_constants_1 = require("./muniIssues.constants");
@@ -21,4 +21,34 @@ const getMuniIssueByJiraKey = async (jiraKey) => {
21
21
  return (0, exports.getMuniIssuesCollection)().findOne({ "jira.key": jiraKey });
22
22
  };
23
23
  exports.getMuniIssueByJiraKey = getMuniIssueByJiraKey;
24
+ // Tracked issues of one client. Issues without a status predate complaint tracking and are excluded.
25
+ const clientIssuesFilter = (clientId, answered) => ({
26
+ clientId,
27
+ status: { $exists: true },
28
+ ...(answered !== undefined && { response: { $exists: answered } }),
29
+ });
30
+ /** A client's tracked issues, newest first. */
31
+ const findMuniIssuesByClient = async (clientId, { answered, skip = 0, limit = 0 } = {}) => {
32
+ return (0, exports.getMuniIssuesCollection)()
33
+ .find(clientIssuesFilter(clientId, answered))
34
+ .sort({ createdAt: -1 })
35
+ .skip(skip)
36
+ .limit(limit)
37
+ .toArray();
38
+ };
39
+ exports.findMuniIssuesByClient = findMuniIssuesByClient;
40
+ const countMuniIssuesByClient = async (clientId, { answered } = {}) => {
41
+ return (0, exports.getMuniIssuesCollection)().countDocuments(clientIssuesFilter(clientId, answered));
42
+ };
43
+ exports.countMuniIssuesByClient = countMuniIssuesByClient;
44
+ /** One tracked issue, only if it belongs to the client. */
45
+ const getMuniIssueByIdForClient = async (id, clientId) => {
46
+ if (!mongodb_1.ObjectId.isValid(id))
47
+ return null;
48
+ return (0, exports.getMuniIssuesCollection)().findOne({
49
+ ...clientIssuesFilter(clientId),
50
+ _id: new mongodb_1.ObjectId(id),
51
+ });
52
+ };
53
+ exports.getMuniIssueByIdForClient = getMuniIssueByIdForClient;
24
54
  //# sourceMappingURL=muniIssues.getters.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.getters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.getters.ts"],"names":[],"mappings":";;;AAAA,qCAA+C;AAC/C,oCAAiC;AAEjC,iEAAgE;AAEzD,MAAM,uBAAuB,GAAG,GAA0B,EAAE;IACjE,OAAO,IAAA,aAAK,GAAE,CAAC,UAAU,CAAY,6CAAsB,CAAC,CAAC;AAC/D,CAAC,CAAC;AAFW,QAAA,uBAAuB,2BAElC;AAEK,MAAM,mBAAmB,GAAG,GAAa,EAAE,CAAC,IAAI,kBAAQ,EAAE,CAAC;AAArD,QAAA,mBAAmB,uBAAkC;AAE3D,MAAM,gBAAgB,GAAG,KAAK,EACnC,EAAU,EACiB,EAAE;IAC7B,iFAAiF;IACjF,IAAI,CAAC,kBAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAA,+BAAuB,GAAE,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,kBAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AACtE,CAAC,CAAC;AANW,QAAA,gBAAgB,oBAM3B;AAEK,MAAM,qBAAqB,GAAG,KAAK,EACxC,OAAe,EACY,EAAE;IAC7B,OAAO,IAAA,+BAAuB,GAAE,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;AACpE,CAAC,CAAC;AAJW,QAAA,qBAAqB,yBAIhC"}
1
+ {"version":3,"file":"muniIssues.getters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.getters.ts"],"names":[],"mappings":";;;AAAA,qCAAuD;AACvD,oCAAiC;AAEjC,iEAAgE;AAEzD,MAAM,uBAAuB,GAAG,GAA0B,EAAE;IACjE,OAAO,IAAA,aAAK,GAAE,CAAC,UAAU,CAAY,6CAAsB,CAAC,CAAC;AAC/D,CAAC,CAAC;AAFW,QAAA,uBAAuB,2BAElC;AAEK,MAAM,mBAAmB,GAAG,GAAa,EAAE,CAAC,IAAI,kBAAQ,EAAE,CAAC;AAArD,QAAA,mBAAmB,uBAAkC;AAE3D,MAAM,gBAAgB,GAAG,KAAK,EACnC,EAAU,EACiB,EAAE;IAC7B,iFAAiF;IACjF,IAAI,CAAC,kBAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAA,+BAAuB,GAAE,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,IAAI,kBAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AACtE,CAAC,CAAC;AANW,QAAA,gBAAgB,oBAM3B;AAEK,MAAM,qBAAqB,GAAG,KAAK,EACxC,OAAe,EACY,EAAE;IAC7B,OAAO,IAAA,+BAAuB,GAAE,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;AACpE,CAAC,CAAC;AAJW,QAAA,qBAAqB,yBAIhC;AAEF,qGAAqG;AACrG,MAAM,kBAAkB,GAAG,CACzB,QAAgB,EAChB,QAAkB,EACC,EAAE,CAAC,CAAC;IACvB,QAAQ;IACR,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;IACzB,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC;CACnE,CAAC,CAAC;AAEH,+CAA+C;AACxC,MAAM,sBAAsB,GAAG,KAAK,EACzC,QAAgB,EAChB,EAAE,QAAQ,EAAE,IAAI,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,KAA4B,EAAE,EACvC,EAAE;IACxB,OAAO,IAAA,+BAAuB,GAAE;SAC7B,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;SAC5C,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC;SACvB,IAAI,CAAC,IAAI,CAAC;SACV,KAAK,CAAC,KAAK,CAAC;SACZ,OAAO,EAAE,CAAC;AACf,CAAC,CAAC;AAVW,QAAA,sBAAsB,0BAUjC;AAEK,MAAM,uBAAuB,GAAG,KAAK,EAC1C,QAAgB,EAChB,EAAE,QAAQ,KAA8C,EAAE,EACzC,EAAE;IACnB,OAAO,IAAA,+BAAuB,GAAE,CAAC,cAAc,CAC7C,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CACvC,CAAC;AACJ,CAAC,CAAC;AAPW,QAAA,uBAAuB,2BAOlC;AAEF,2DAA2D;AACpD,MAAM,yBAAyB,GAAG,KAAK,EAC5C,EAAU,EACV,QAAgB,EACW,EAAE;IAC7B,IAAI,CAAC,kBAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,IAAA,+BAAuB,GAAE,CAAC,OAAO,CAAC;QACvC,GAAG,kBAAkB,CAAC,QAAQ,CAAC;QAC/B,GAAG,EAAE,IAAI,kBAAQ,CAAC,EAAE,CAAC;KACtB,CAAC,CAAC;AACL,CAAC,CAAC;AATW,QAAA,yBAAyB,6BASpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.schema.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAO1D,eAAO,MAAM,oBAAoB,EAAE,QAmDlC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,gBAAgB,EAK/C,CAAC"}
1
+ {"version":3,"file":"muniIssues.schema.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAQ1D,eAAO,MAAM,oBAAoB,EAAE,QA8DlC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,gBAAgB,EAO/C,CAAC"}
@@ -52,11 +52,24 @@ exports.muniIssuesJsonSchema = {
52
52
  },
53
53
  },
54
54
  createdAt: { bsonType: "date" },
55
+ status: { enum: [...muniIssues_constants_1.MUNI_ISSUE_STATUSES] },
56
+ response: {
57
+ bsonType: "object",
58
+ required: ["text", "createdAt"],
59
+ properties: {
60
+ text: { bsonType: "string", maxLength: muniIssues_constants_1.MUNI_ISSUE_TEXT_MAX_LENGTH },
61
+ imageUrl: { bsonType: "string" },
62
+ createdAt: { bsonType: "date" },
63
+ },
64
+ },
65
+ updatedAt: { bsonType: "date" },
55
66
  },
56
67
  };
57
68
  exports.muniIssuesIndexes = [
58
69
  { key: { callSid: 1 }, name: "callSid_1" },
59
70
  { key: { createdAt: -1 }, name: "createdAt_-1" },
71
+ // Complaint list: one municipality's issues, newest first.
72
+ { key: { clientId: 1, createdAt: -1 }, name: "clientId_1_createdAt_-1" },
60
73
  // Unique: one stored document per Jira ticket, guarding against duplicate writes.
61
74
  { key: { "jira.key": 1 }, name: "jira_key_unique", unique: true },
62
75
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.schema.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":";;;AACA,iEAIgC;AAEnB,QAAA,oBAAoB,GAAa;IAC5C,QAAQ,EAAE,QAAQ;IAClB,QAAQ,EAAE;QACR,SAAS;QACT,UAAU;QACV,UAAU;QACV,cAAc;QACd,MAAM;QACN,WAAW;KACZ;IACD,UAAU,EAAE;QACV,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAC/B,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAChC,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAChC,YAAY,EAAE;YACZ,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE;gBACR,YAAY;gBACZ,YAAY;gBACZ,kBAAkB;gBAClB,iBAAiB;aAClB;YACD,UAAU,EAAE;gBACV,UAAU,EAAE;oBACV,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,UAAU,EAAE;oBACV,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,gBAAgB,EAAE;oBAChB,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,eAAe,EAAE;oBACf,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;aACF;SACF;QACD,IAAI,EAAE;YACJ,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC;YAC/B,UAAU,EAAE;gBACV,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBAC3B,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,+CAAwB,CAAC,EAAE;aACpD;SACF;QACD,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;KAChC;CACF,CAAC;AAEW,QAAA,iBAAiB,GAAuB;IACnD,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;IAC1C,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;IAChD,kFAAkF;IAClF,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;CAClE,CAAC"}
1
+ {"version":3,"file":"muniIssues.schema.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":";;;AACA,iEAKgC;AAEnB,QAAA,oBAAoB,GAAa;IAC5C,QAAQ,EAAE,QAAQ;IAClB,QAAQ,EAAE;QACR,SAAS;QACT,UAAU;QACV,UAAU;QACV,cAAc;QACd,MAAM;QACN,WAAW;KACZ;IACD,UAAU,EAAE;QACV,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAC/B,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAChC,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAChC,YAAY,EAAE;YACZ,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE;gBACR,YAAY;gBACZ,YAAY;gBACZ,kBAAkB;gBAClB,iBAAiB;aAClB;YACD,UAAU,EAAE;gBACV,UAAU,EAAE;oBACV,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,UAAU,EAAE;oBACV,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,gBAAgB,EAAE;oBAChB,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;gBACD,eAAe,EAAE;oBACf,QAAQ,EAAE,QAAQ;oBAClB,SAAS,EAAE,iDAA0B;iBACtC;aACF;SACF;QACD,IAAI,EAAE;YACJ,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC;YAC/B,UAAU,EAAE;gBACV,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBAC3B,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,+CAAwB,CAAC,EAAE;aACpD;SACF;QACD,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;QAC/B,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,0CAAmB,CAAC,EAAE;QAC1C,QAAQ,EAAE;YACR,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC;YAC/B,UAAU,EAAE;gBACV,IAAI,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,iDAA0B,EAAE;gBACnE,QAAQ,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBAChC,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;aAChC;SACF;QACD,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;KAChC;CACF,CAAC;AAEW,QAAA,iBAAiB,GAAuB;IACnD,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;IAC1C,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE;IAChD,2DAA2D;IAC3D,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACxE,kFAAkF;IAClF,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE;CAClE,CAAC"}
@@ -1,4 +1,8 @@
1
1
  import { ObjectId } from "mongodb";
2
- import { CreateMuniIssueInput } from "./muniIssues.types";
2
+ import { CreateMuniIssueInput, MuniIssueStatus, SetMuniIssueResponseInput } from "./muniIssues.types";
3
3
  export declare const createMuniIssue: (issueData: CreateMuniIssueInput, id: ObjectId) => Promise<ObjectId>;
4
+ /** Changes the status of an unanswered issue. Returns false if none matched. */
5
+ export declare const updateMuniIssueStatus: (id: string, clientId: string, status: MuniIssueStatus) => Promise<boolean>;
6
+ /** Saves the single TalkPilot response, which locks the issue. Returns false if none matched. */
7
+ export declare const setMuniIssueResponse: (id: string, clientId: string, { status, text, imageUrl }: SetMuniIssueResponseInput) => Promise<boolean>;
4
8
  //# sourceMappingURL=muniIssues.setters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.setters.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAa,MAAM,oBAAoB,CAAC;AAKrE,eAAO,MAAM,eAAe,GAC1B,WAAW,oBAAoB,EAC/B,IAAI,QAAQ,KACX,OAAO,CAAC,QAAQ,CAclB,CAAC"}
1
+ {"version":3,"file":"muniIssues.setters.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EACL,oBAAoB,EAEpB,eAAe,EACf,yBAAyB,EAC1B,MAAM,oBAAoB,CAAC;AAK5B,eAAO,MAAM,eAAe,GAC1B,WAAW,oBAAoB,EAC/B,IAAI,QAAQ,KACX,OAAO,CAAC,QAAQ,CAiBlB,CAAC;AAWF,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,GAChC,IAAI,MAAM,EACV,UAAU,MAAM,EAChB,QAAQ,eAAe,KACtB,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF,iGAAiG;AACjG,eAAO,MAAM,oBAAoB,GAC/B,IAAI,MAAM,EACV,UAAU,MAAM,EAChB,4BAA4B,yBAAyB,KACpD,OAAO,CAAC,OAAO,CAgBjB,CAAC"}
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createMuniIssue = void 0;
3
+ exports.setMuniIssueResponse = exports.updateMuniIssueStatus = exports.createMuniIssue = void 0;
4
+ const mongodb_1 = require("mongodb");
4
5
  const muniIssues_getters_1 = require("./muniIssues.getters");
5
6
  const muniIssues_setup_1 = require("./muniIssues.setup");
6
7
  // Required `id`: MIS pre-generates it (generateMuniIssueId) and embeds it in the Jira ticket — the two-way link.
7
8
  const createMuniIssue = async (issueData, id) => {
8
9
  await (0, muniIssues_setup_1.ensureMuniIssuesCollectionOnce)();
10
+ const now = new Date();
9
11
  const issue = {
10
12
  _id: id,
11
13
  ...issueData,
@@ -13,10 +15,45 @@ const createMuniIssue = async (issueData, id) => {
13
15
  ...issueData.jira,
14
16
  syncStatus: issueData.jira.syncStatus ?? "created",
15
17
  },
16
- createdAt: new Date(),
18
+ status: "in_review",
19
+ createdAt: now,
20
+ updatedAt: now,
17
21
  };
18
22
  const { insertedId } = await (0, muniIssues_getters_1.getMuniIssuesCollection)().insertOne(issue);
19
23
  return insertedId;
20
24
  };
21
25
  exports.createMuniIssue = createMuniIssue;
26
+ // An issue of this client that is tracked (has a status) and not yet answered.
27
+ // The `response` condition is the lock: once answered, no further writes match.
28
+ const openIssueFilter = (id, clientId) => ({
29
+ _id: new mongodb_1.ObjectId(id),
30
+ clientId,
31
+ status: { $exists: true },
32
+ response: { $exists: false },
33
+ });
34
+ /** Changes the status of an unanswered issue. Returns false if none matched. */
35
+ const updateMuniIssueStatus = async (id, clientId, status) => {
36
+ if (!mongodb_1.ObjectId.isValid(id))
37
+ return false;
38
+ await (0, muniIssues_setup_1.ensureMuniIssuesCollectionOnce)();
39
+ const { matchedCount } = await (0, muniIssues_getters_1.getMuniIssuesCollection)().updateOne(openIssueFilter(id, clientId), { $set: { status, updatedAt: new Date() } });
40
+ return matchedCount > 0;
41
+ };
42
+ exports.updateMuniIssueStatus = updateMuniIssueStatus;
43
+ /** Saves the single TalkPilot response, which locks the issue. Returns false if none matched. */
44
+ const setMuniIssueResponse = async (id, clientId, { status, text, imageUrl }) => {
45
+ if (!mongodb_1.ObjectId.isValid(id))
46
+ return false;
47
+ await (0, muniIssues_setup_1.ensureMuniIssuesCollectionOnce)();
48
+ const now = new Date();
49
+ const { matchedCount } = await (0, muniIssues_getters_1.getMuniIssuesCollection)().updateOne(openIssueFilter(id, clientId), {
50
+ $set: {
51
+ status,
52
+ response: { text, ...(imageUrl && { imageUrl }), createdAt: now },
53
+ updatedAt: now,
54
+ },
55
+ });
56
+ return matchedCount > 0;
57
+ };
58
+ exports.setMuniIssueResponse = setMuniIssueResponse;
22
59
  //# sourceMappingURL=muniIssues.setters.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.setters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":";;;AAEA,6DAA+D;AAC/D,yDAAoE;AAEpE,iHAAiH;AAC1G,MAAM,eAAe,GAAG,KAAK,EAClC,SAA+B,EAC/B,EAAY,EACO,EAAE;IACrB,MAAM,IAAA,iDAA8B,GAAE,CAAC;IAEvC,MAAM,KAAK,GAAc;QACvB,GAAG,EAAE,EAAE;QACP,GAAG,SAAS;QACZ,IAAI,EAAE;YACJ,GAAG,SAAS,CAAC,IAAI;YACjB,UAAU,EAAE,SAAS,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS;SACnD;QACD,SAAS,EAAE,IAAI,IAAI,EAAE;KACtB,CAAC;IACF,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,4CAAuB,GAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxE,OAAO,UAAU,CAAC;AACpB,CAAC,CAAC;AAjBW,QAAA,eAAe,mBAiB1B"}
1
+ {"version":3,"file":"muniIssues.setters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":";;;AAAA,qCAA2C;AAO3C,6DAA+D;AAC/D,yDAAoE;AAEpE,iHAAiH;AAC1G,MAAM,eAAe,GAAG,KAAK,EAClC,SAA+B,EAC/B,EAAY,EACO,EAAE;IACrB,MAAM,IAAA,iDAA8B,GAAE,CAAC;IAEvC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,KAAK,GAAc;QACvB,GAAG,EAAE,EAAE;QACP,GAAG,SAAS;QACZ,IAAI,EAAE;YACJ,GAAG,SAAS,CAAC,IAAI;YACjB,UAAU,EAAE,SAAS,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS;SACnD;QACD,MAAM,EAAE,WAAW;QACnB,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG;KACf,CAAC;IACF,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,4CAAuB,GAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxE,OAAO,UAAU,CAAC;AACpB,CAAC,CAAC;AApBW,QAAA,eAAe,mBAoB1B;AAEF,+EAA+E;AAC/E,gFAAgF;AAChF,MAAM,eAAe,GAAG,CAAC,EAAU,EAAE,QAAgB,EAAqB,EAAE,CAAC,CAAC;IAC5E,GAAG,EAAE,IAAI,kBAAQ,CAAC,EAAE,CAAC;IACrB,QAAQ;IACR,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;IACzB,QAAQ,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE;CAC7B,CAAC,CAAC;AAEH,gFAAgF;AACzE,MAAM,qBAAqB,GAAG,KAAK,EACxC,EAAU,EACV,QAAgB,EAChB,MAAuB,EACL,EAAE;IACpB,IAAI,CAAC,kBAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,MAAM,IAAA,iDAA8B,GAAE,CAAC;IAEvC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,IAAA,4CAAuB,GAAE,CAAC,SAAS,CAChE,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,EAC7B,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE,CAC5C,CAAC;IACF,OAAO,YAAY,GAAG,CAAC,CAAC;AAC1B,CAAC,CAAC;AAbW,QAAA,qBAAqB,yBAahC;AAEF,iGAAiG;AAC1F,MAAM,oBAAoB,GAAG,KAAK,EACvC,EAAU,EACV,QAAgB,EAChB,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAA6B,EACnC,EAAE;IACpB,IAAI,CAAC,kBAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,MAAM,IAAA,iDAA8B,GAAE,CAAC;IAEvC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,IAAA,4CAAuB,GAAE,CAAC,SAAS,CAChE,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,EAC7B;QACE,IAAI,EAAE;YACJ,MAAM;YACN,QAAQ,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE;YACjE,SAAS,EAAE,GAAG;SACf;KACF,CACF,CAAC;IACF,OAAO,YAAY,GAAG,CAAC,CAAC;AAC1B,CAAC,CAAC;AApBW,QAAA,oBAAoB,wBAoB/B"}
@@ -1,7 +1,14 @@
1
1
  import { ObjectId } from "mongodb";
2
2
  import { CityName } from "../utils/types";
3
- import { MUNI_ISSUE_SYNC_STATUSES } from "./muniIssues.constants";
3
+ import { MUNI_ISSUE_STATUSES, MUNI_ISSUE_SYNC_STATUSES } from "./muniIssues.constants";
4
4
  export type MuniIssueSyncStatus = (typeof MUNI_ISSUE_SYNC_STATUSES)[number];
5
+ export type MuniIssueStatus = (typeof MUNI_ISSUE_STATUSES)[number];
6
+ /** The single TalkPilot response; once set, the complaint is locked. */
7
+ export type MuniIssueResponse = {
8
+ text: string;
9
+ imageUrl?: string;
10
+ createdAt: Date;
11
+ };
5
12
  export type MuniIssueContent = {
6
13
  issuerName: string;
7
14
  issuerRole: string;
@@ -20,11 +27,25 @@ export type MuniIssue = {
20
27
  issueContent: MuniIssueContent;
21
28
  jira: MuniIssueJira;
22
29
  createdAt: Date;
30
+ status?: MuniIssueStatus;
31
+ response?: MuniIssueResponse;
32
+ updatedAt?: Date;
23
33
  };
24
- export type CreateMuniIssueInput = Omit<MuniIssue, "_id" | "createdAt" | "jira"> & {
34
+ export type CreateMuniIssueInput = Omit<MuniIssue, "_id" | "createdAt" | "jira" | "status" | "response" | "updatedAt"> & {
25
35
  jira: {
26
36
  key: string;
27
37
  syncStatus?: MuniIssueSyncStatus;
28
38
  };
29
39
  };
40
+ export type SetMuniIssueResponseInput = {
41
+ status: MuniIssueStatus;
42
+ text: string;
43
+ imageUrl?: string;
44
+ };
45
+ /** `answered` filters by whether a TalkPilot response exists; omit it for all issues. */
46
+ export type FindMuniIssuesOptions = {
47
+ answered?: boolean;
48
+ skip?: number;
49
+ limit?: number;
50
+ };
30
51
  //# sourceMappingURL=muniIssues.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"muniIssues.types.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAElE,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,mBAAmB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,QAAQ,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAIF,MAAM,MAAM,oBAAoB,GAAG,IAAI,CACrC,SAAS,EACT,KAAK,GAAG,WAAW,GAAG,MAAM,CAC7B,GAAG;IACF,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;KAAE,CAAC;CACzD,CAAC"}
1
+ {"version":3,"file":"muniIssues.types.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,wEAAwE;AACxE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,mBAAmB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,QAAQ,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC;IAGhB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,CAAC;AAIF,MAAM,MAAM,oBAAoB,GAAG,IAAI,CACrC,SAAS,EACT,KAAK,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CACnE,GAAG;IACF,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;KAAE,CAAC;CACzD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talkpilot/core-db",
3
- "version": "1.3.71",
3
+ "version": "1.3.72",
4
4
  "description": "Core database package for centralized connections and ORM integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,60 @@
1
+ import { createMuniIssue, setMuniIssueResponse } from "../muniIssues.setters";
2
+ import {
3
+ countMuniIssuesByClient,
4
+ findMuniIssuesByClient,
5
+ generateMuniIssueId,
6
+ getMuniIssueByIdForClient,
7
+ getMuniIssuesCollection,
8
+ } from "../muniIssues.getters";
9
+ import { createMuniIssueInput } from "../../../test-utils/factories";
10
+
11
+ const CLIENT = "client-a";
12
+
13
+ const createIssue = async (clientId = CLIENT) =>
14
+ (await createMuniIssue(createMuniIssueInput({ clientId }), generateMuniIssueId())).toString();
15
+
16
+ describe("muniIssues getters (complaint tracking)", () => {
17
+ describe("findMuniIssuesByClient / countMuniIssuesByClient", () => {
18
+ it("should return only the client's tracked issues, newest first", async () => {
19
+ const older = await createIssue();
20
+ const newer = await createIssue();
21
+ await createIssue("client-b");
22
+ // An issue created before complaint tracking has no status.
23
+ await getMuniIssuesCollection().insertOne({
24
+ ...createMuniIssueInput({ clientId: CLIENT }),
25
+ _id: generateMuniIssueId(),
26
+ jira: { key: "CP-legacy", syncStatus: "created" },
27
+ createdAt: new Date(),
28
+ });
29
+
30
+ const issues = await findMuniIssuesByClient(CLIENT);
31
+
32
+ expect(issues.map((i) => i._id.toString())).toEqual([newer, older]);
33
+ expect(await countMuniIssuesByClient(CLIENT)).toBe(2);
34
+ });
35
+
36
+ it("should filter by answered and apply skip/limit", async () => {
37
+ const answered = await createIssue();
38
+ await createIssue();
39
+ await createIssue();
40
+ await setMuniIssueResponse(answered, CLIENT, { status: "resolved", text: "Done" });
41
+
42
+ expect(await countMuniIssuesByClient(CLIENT, { answered: true })).toBe(1);
43
+ expect(await countMuniIssuesByClient(CLIENT, { answered: false })).toBe(2);
44
+ expect(
45
+ (await findMuniIssuesByClient(CLIENT, { answered: true }))[0]._id.toString(),
46
+ ).toBe(answered);
47
+ expect(await findMuniIssuesByClient(CLIENT, { skip: 1, limit: 1 })).toHaveLength(1);
48
+ });
49
+ });
50
+
51
+ describe("getMuniIssueByIdForClient", () => {
52
+ it("should return the issue only for its own client", async () => {
53
+ const id = await createIssue();
54
+
55
+ expect((await getMuniIssueByIdForClient(id, CLIENT))?._id.toString()).toBe(id);
56
+ expect(await getMuniIssueByIdForClient(id, "client-b")).toBeNull();
57
+ expect(await getMuniIssueByIdForClient("not-a-valid-id", CLIENT)).toBeNull();
58
+ });
59
+ });
60
+ });
@@ -1,4 +1,8 @@
1
- import { createMuniIssue } from "../muniIssues.setters";
1
+ import {
2
+ createMuniIssue,
3
+ setMuniIssueResponse,
4
+ updateMuniIssueStatus,
5
+ } from "../muniIssues.setters";
2
6
  import {
3
7
  generateMuniIssueId,
4
8
  getMuniIssueById,
@@ -25,6 +29,18 @@ describe("muniIssues setters", () => {
25
29
  expect(saved?.createdAt).toBeInstanceOf(Date);
26
30
  });
27
31
 
32
+ it("should start the issue as in_review with updatedAt and no response", async () => {
33
+ const insertedId = await createMuniIssue(
34
+ createMuniIssueInput(),
35
+ generateMuniIssueId(),
36
+ );
37
+
38
+ const saved = await getMuniIssueById(insertedId.toString());
39
+ expect(saved?.status).toBe("in_review");
40
+ expect(saved?.updatedAt).toBeInstanceOf(Date);
41
+ expect(saved?.response).toBeUndefined();
42
+ });
43
+
28
44
  it("should default jira.syncStatus to created when not provided", async () => {
29
45
  const id = generateMuniIssueId();
30
46
 
@@ -91,4 +107,61 @@ describe("muniIssues setters", () => {
91
107
  expect(await getMuniIssueByJiraKey("CP-nope")).toBeNull();
92
108
  });
93
109
  });
110
+
111
+ describe("updateMuniIssueStatus", () => {
112
+ it("should change the status of an unanswered issue", async () => {
113
+ const input = createMuniIssueInput();
114
+ const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
115
+
116
+ expect(await updateMuniIssueStatus(id, input.clientId, "in_progress")).toBe(true);
117
+ expect((await getMuniIssueById(id))?.status).toBe("in_progress");
118
+ });
119
+
120
+ it("should not touch another client's issue or a malformed id", async () => {
121
+ const id = (
122
+ await createMuniIssue(createMuniIssueInput(), generateMuniIssueId())
123
+ ).toString();
124
+
125
+ expect(await updateMuniIssueStatus(id, "other-client", "resolved")).toBe(false);
126
+ expect(await updateMuniIssueStatus("not-a-valid-id", "any", "resolved")).toBe(false);
127
+ expect((await getMuniIssueById(id))?.status).toBe("in_review");
128
+ });
129
+ });
130
+
131
+ describe("setMuniIssueResponse", () => {
132
+ it("should save the response and status", async () => {
133
+ const input = createMuniIssueInput();
134
+ const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
135
+
136
+ const saved = await setMuniIssueResponse(id, input.clientId, {
137
+ status: "resolved",
138
+ text: "Fixed",
139
+ imageUrl: "https://example.com/a.png",
140
+ });
141
+
142
+ expect(saved).toBe(true);
143
+ const issue = await getMuniIssueById(id);
144
+ expect(issue?.status).toBe("resolved");
145
+ expect(issue?.response).toMatchObject({
146
+ text: "Fixed",
147
+ imageUrl: "https://example.com/a.png",
148
+ });
149
+ expect(issue?.response?.createdAt).toBeInstanceOf(Date);
150
+ });
151
+
152
+ it("should lock the issue: no second response and no status change", async () => {
153
+ const input = createMuniIssueInput();
154
+ const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
155
+ await setMuniIssueResponse(id, input.clientId, { status: "resolved", text: "First" });
156
+
157
+ expect(
158
+ await setMuniIssueResponse(id, input.clientId, { status: "in_progress", text: "Second" }),
159
+ ).toBe(false);
160
+ expect(await updateMuniIssueStatus(id, input.clientId, "in_progress")).toBe(false);
161
+
162
+ const issue = await getMuniIssueById(id);
163
+ expect(issue?.status).toBe("resolved");
164
+ expect(issue?.response?.text).toBe("First");
165
+ });
166
+ });
94
167
  });
@@ -28,6 +28,7 @@ describe("muniIssues setup", () => {
28
28
  expect.arrayContaining([
29
29
  "callSid_1",
30
30
  "createdAt_-1",
31
+ "clientId_1_createdAt_-1",
31
32
  "jira_key_unique",
32
33
  ]),
33
34
  );
@@ -3,6 +3,7 @@ export * from "./muniIssues.setters";
3
3
  export * from "./muniIssues.setup";
4
4
  export {
5
5
  MUNI_ISSUES_COLLECTION,
6
+ MUNI_ISSUE_STATUSES,
6
7
  MUNI_ISSUE_SYNC_STATUSES,
7
8
  MUNI_ISSUE_TEXT_MAX_LENGTH,
8
9
  MUNI_ISSUE_NAME_MAX_LENGTH,
@@ -13,5 +14,9 @@ export type {
13
14
  MuniIssueContent,
14
15
  MuniIssueJira,
15
16
  MuniIssueSyncStatus,
17
+ MuniIssueStatus,
18
+ MuniIssueResponse,
16
19
  CreateMuniIssueInput,
20
+ SetMuniIssueResponseInput,
21
+ FindMuniIssuesOptions,
17
22
  } from "./muniIssues.types";
@@ -4,6 +4,14 @@ export const MUNI_ISSUES_COLLECTION = "muniIssues";
4
4
  /** Sync state of the issue's Jira ticket at write time. */
5
5
  export const MUNI_ISSUE_SYNC_STATUSES = ["created", "failed"] as const;
6
6
 
7
+ /** Customer-facing complaint status, set manually by the TalkPilot team. */
8
+ export const MUNI_ISSUE_STATUSES = [
9
+ "in_review",
10
+ "in_progress",
11
+ "resolved",
12
+ "no_issue_found",
13
+ ] as const;
14
+
7
15
  /** Max length for the long free-text fields — well above the ~2000-char target. */
8
16
  export const MUNI_ISSUE_TEXT_MAX_LENGTH = 5000;
9
17
 
@@ -1,6 +1,6 @@
1
- import { Collection, ObjectId } from "mongodb";
1
+ import { Collection, Filter, ObjectId } from "mongodb";
2
2
  import { getDb } from "../index";
3
- import { MuniIssue } from "./muniIssues.types";
3
+ import { FindMuniIssuesOptions, MuniIssue } from "./muniIssues.types";
4
4
  import { MUNI_ISSUES_COLLECTION } from "./muniIssues.constants";
5
5
 
6
6
  export const getMuniIssuesCollection = (): Collection<MuniIssue> => {
@@ -22,3 +22,47 @@ export const getMuniIssueByJiraKey = async (
22
22
  ): Promise<MuniIssue | null> => {
23
23
  return getMuniIssuesCollection().findOne({ "jira.key": jiraKey });
24
24
  };
25
+
26
+ // Tracked issues of one client. Issues without a status predate complaint tracking and are excluded.
27
+ const clientIssuesFilter = (
28
+ clientId: string,
29
+ answered?: boolean,
30
+ ): Filter<MuniIssue> => ({
31
+ clientId,
32
+ status: { $exists: true },
33
+ ...(answered !== undefined && { response: { $exists: answered } }),
34
+ });
35
+
36
+ /** A client's tracked issues, newest first. */
37
+ export const findMuniIssuesByClient = async (
38
+ clientId: string,
39
+ { answered, skip = 0, limit = 0 }: FindMuniIssuesOptions = {},
40
+ ): Promise<MuniIssue[]> => {
41
+ return getMuniIssuesCollection()
42
+ .find(clientIssuesFilter(clientId, answered))
43
+ .sort({ createdAt: -1 })
44
+ .skip(skip)
45
+ .limit(limit)
46
+ .toArray();
47
+ };
48
+
49
+ export const countMuniIssuesByClient = async (
50
+ clientId: string,
51
+ { answered }: Pick<FindMuniIssuesOptions, "answered"> = {},
52
+ ): Promise<number> => {
53
+ return getMuniIssuesCollection().countDocuments(
54
+ clientIssuesFilter(clientId, answered),
55
+ );
56
+ };
57
+
58
+ /** One tracked issue, only if it belongs to the client. */
59
+ export const getMuniIssueByIdForClient = async (
60
+ id: string,
61
+ clientId: string,
62
+ ): Promise<MuniIssue | null> => {
63
+ if (!ObjectId.isValid(id)) return null;
64
+ return getMuniIssuesCollection().findOne({
65
+ ...clientIssuesFilter(clientId),
66
+ _id: new ObjectId(id),
67
+ });
68
+ };