@talkpilot/core-db 1.3.85 → 1.3.87
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/README.md +20 -0
- package/dist/municipal/muniIssues/index.d.ts +3 -2
- package/dist/municipal/muniIssues/index.d.ts.map +1 -1
- package/dist/municipal/muniIssues/index.js +4 -1
- package/dist/municipal/muniIssues/index.js.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts +2 -0
- package/dist/municipal/muniIssues/muniIssues.constants.d.ts.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.constants.js +8 -1
- package/dist/municipal/muniIssues/muniIssues.constants.js.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts +4 -1
- package/dist/municipal/muniIssues/muniIssues.getters.d.ts.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.getters.js +35 -1
- package/dist/municipal/muniIssues/muniIssues.getters.js.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.schema.d.ts.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.schema.js +17 -0
- package/dist/municipal/muniIssues/muniIssues.schema.js.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts +3 -1
- package/dist/municipal/muniIssues/muniIssues.setters.d.ts.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.setters.js +46 -2
- package/dist/municipal/muniIssues/muniIssues.setters.js.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.types.d.ts +22 -2
- package/dist/municipal/muniIssues/muniIssues.types.d.ts.map +1 -1
- package/dist/municipal/muniIssues/muniIssues.utils.d.ts +3 -0
- package/dist/municipal/muniIssues/muniIssues.utils.d.ts.map +1 -0
- package/dist/municipal/muniIssues/muniIssues.utils.js +6 -0
- package/dist/municipal/muniIssues/muniIssues.utils.js.map +1 -0
- package/package.json +1 -1
- package/src/municipal/muniIssues/__tests__/muniIssues.getters.spec.ts +87 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.setters.spec.ts +126 -1
- package/src/municipal/muniIssues/__tests__/muniIssues.setup.spec.ts +1 -0
- package/src/municipal/muniIssues/__tests__/muniIssues.utils.spec.ts +17 -0
- package/src/municipal/muniIssues/index.ts +7 -0
- package/src/municipal/muniIssues/muniIssues.constants.ts +9 -0
- package/src/municipal/muniIssues/muniIssues.getters.ts +51 -2
- package/src/municipal/muniIssues/muniIssues.schema.ts +19 -0
- package/src/municipal/muniIssues/muniIssues.setters.ts +66 -3
- package/src/municipal/muniIssues/muniIssues.types.ts +34 -4
- package/src/municipal/muniIssues/muniIssues.utils.ts +6 -0
package/README.md
CHANGED
|
@@ -186,6 +186,9 @@ Refer to `DEVELOPMENT.md` for the full walkthrough, token instructions, and fact
|
|
|
186
186
|
| 1.3.69 | Municipal `residents` collection: optional resident fields (`cityName`, `firstName`, `lastName`, `idNumber`, `phoneNumber`, `address`) with exact-match getters by phone, by idNumber, and by phone+idNumber (city-scoped). Schema + indexes exported; collection is created out of band. |
|
|
187
187
|
| 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`. |
|
|
188
188
|
| 1.3.74 | Added `human` to `CallEndedByValues` (CP-1873): a human agent ended the call, after a transfer or in a conference. |
|
|
189
|
+
| 1.3.82 | Complaint tracking on `muniIssues` (CP-1888): optional `status` / `response` / `updatedAt`; an editable TalkPilot response with up to `MUNI_ISSUE_MAX_RESPONSE_IMAGES` (5) `response.imageUrls` and `response.updatedAt`; `updateMuniIssueStatus`, `setMuniIssueResponse`, `findMuniIssuesByClient`, `countMuniIssuesByClient`, `getMuniIssueByIdForClient`; index `clientId_1_createdAt_-1`. |
|
|
190
|
+
| 1.3.85 | Single-note writes on `contextNotes` (CP-1998): append, update, or remove one entry without replacing the rest of the list. |
|
|
191
|
+
|
|
189
192
|
|
|
190
193
|
### 1.1.9
|
|
191
194
|
|
|
@@ -834,6 +837,23 @@ New `CallEndedByValues.Human` (`"human"`): a human agent ended the call, either
|
|
|
834
837
|
|
|
835
838
|
No — purely additive. **TalkPilot Server** should upgrade to **1.3.74** to write it.
|
|
836
839
|
|
|
840
|
+
### 1.3.85 — Single context-note writes (CP-1998)
|
|
841
|
+
|
|
842
|
+
Append, update, or remove one note on a client/product document without replacing the rest of the `notes` array. `appendContextNoteEntry` creates the document when none exists yet. `setContextNoteEntries` still replaces the whole list.
|
|
843
|
+
|
|
844
|
+
**Added**
|
|
845
|
+
|
|
846
|
+
| Export | Purpose |
|
|
847
|
+
| --- | --- |
|
|
848
|
+
| `ContextNoteEntryWrite` | Fields the caller sets: `title`, `content`, `activeFrom`, `expiresAt`. The getter assigns `_id`, `createdAt`, and `updatedAt`. |
|
|
849
|
+
| `appendContextNoteEntry(clientId, product, entry, now?)` | Pushes one note and upserts the document. Returns the stored entry. |
|
|
850
|
+
| `updateContextNoteEntryFields(clientId, product, noteId, entry, now?)` | Replaces one note's writable fields and `updatedAt`. Leaves `createdAt` and the other notes alone. Returns `null` when the note is missing. |
|
|
851
|
+
| `pullContextNoteEntry(clientId, product, noteId)` | Removes one note by id. Returns `false` when nothing changed. |
|
|
852
|
+
|
|
853
|
+
**Do I need to update my app?**
|
|
854
|
+
|
|
855
|
+
No — purely additive. Use these instead of `setContextNoteEntries` when only one note changes.
|
|
856
|
+
|
|
837
857
|
## 🛠 CI/CD & deployment
|
|
838
858
|
|
|
839
859
|
- This package is consumed by Cloud Build-based services (TalkPilot Server, MIS, CIS) as a dependency when Docker images are built. Keep `dist/` in sync with your builds because the compiled artifact is what downstream services install.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from "./muniIssues.getters";
|
|
2
2
|
export * from "./muniIssues.setters";
|
|
3
3
|
export * from "./muniIssues.setup";
|
|
4
|
-
export
|
|
4
|
+
export * from "./muniIssues.utils";
|
|
5
|
+
export { MUNI_ISSUES_COLLECTION, MUNI_ISSUE_MAX_RESPONSE_IMAGES, MUNI_ISSUE_STATUSES, MUNI_ISSUE_SYNC_STATUSES, MUNI_ISSUE_TEXT_MAX_LENGTH, MUNI_ISSUE_NAME_MAX_LENGTH, } from "./muniIssues.constants";
|
|
5
6
|
export { muniIssuesJsonSchema, muniIssuesIndexes } from "./muniIssues.schema";
|
|
6
|
-
export type { MuniIssue, MuniIssueContent, MuniIssueJira, MuniIssueSyncStatus, CreateMuniIssueInput, } from "./muniIssues.types";
|
|
7
|
+
export type { MuniIssue, MuniIssueContent, MuniIssueJira, MuniIssueSyncStatus, MuniIssueStatus, MuniIssueResponse, CreateMuniIssueInput, SetMuniIssueResponseInput, FindMuniIssuesOptions, } from "./muniIssues.types";
|
|
7
8
|
//# 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,
|
|
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,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,EAC9B,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,15 @@ 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_ISSUE_MAX_RESPONSE_IMAGES = 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
|
+
__exportStar(require("./muniIssues.utils"), exports);
|
|
21
22
|
var muniIssues_constants_1 = require("./muniIssues.constants");
|
|
22
23
|
Object.defineProperty(exports, "MUNI_ISSUES_COLLECTION", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUES_COLLECTION; } });
|
|
24
|
+
Object.defineProperty(exports, "MUNI_ISSUE_MAX_RESPONSE_IMAGES", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_MAX_RESPONSE_IMAGES; } });
|
|
25
|
+
Object.defineProperty(exports, "MUNI_ISSUE_STATUSES", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_STATUSES; } });
|
|
23
26
|
Object.defineProperty(exports, "MUNI_ISSUE_SYNC_STATUSES", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_SYNC_STATUSES; } });
|
|
24
27
|
Object.defineProperty(exports, "MUNI_ISSUE_TEXT_MAX_LENGTH", { enumerable: true, get: function () { return muniIssues_constants_1.MUNI_ISSUE_TEXT_MAX_LENGTH; } });
|
|
25
28
|
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,+
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,uDAAqC;AACrC,qDAAmC;AACnC,qDAAmC;AACnC,+DAOgC;AAN9B,8HAAA,sBAAsB,OAAA;AACtB,sIAAA,8BAA8B,OAAA;AAC9B,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
|
+
export declare const MUNI_ISSUE_STATUSES: readonly ["in_review", "in_progress", "resolved", "no_issue_found"];
|
|
6
|
+
export declare const MUNI_ISSUE_MAX_RESPONSE_IMAGES = 5;
|
|
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,eAAO,MAAM,mBAAmB,qEAKtB,CAAC;AAEX,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,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_MAX_RESPONSE_IMAGES = 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
|
+
exports.MUNI_ISSUE_STATUSES = [
|
|
9
|
+
"in_review",
|
|
10
|
+
"in_progress",
|
|
11
|
+
"resolved",
|
|
12
|
+
"no_issue_found",
|
|
13
|
+
];
|
|
14
|
+
exports.MUNI_ISSUE_MAX_RESPONSE_IMAGES = 5;
|
|
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;
|
|
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;AAE1D,QAAA,mBAAmB,GAAG;IACjC,WAAW;IACX,aAAa;IACb,UAAU;IACV,gBAAgB;CACR,CAAC;AAEE,QAAA,8BAA8B,GAAG,CAAC,CAAC;AAEhD,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,10 @@
|
|
|
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
|
+
export declare const findMuniIssuesByClient: (clientId: string, { answered, skip, limit }?: FindMuniIssuesOptions) => Promise<MuniIssue[]>;
|
|
8
|
+
export declare const countMuniIssuesByClient: (clientId: string, { answered }?: Pick<FindMuniIssuesOptions, "answered">) => Promise<number>;
|
|
9
|
+
export declare const getMuniIssueByIdForClient: (id: string, clientId: string) => Promise<MuniIssue | null>;
|
|
7
10
|
//# 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,
|
|
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;AAmBF,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,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,38 @@ const getMuniIssueByJiraKey = async (jiraKey) => {
|
|
|
21
21
|
return (0, exports.getMuniIssuesCollection)().findOne({ "jira.key": jiraKey });
|
|
22
22
|
};
|
|
23
23
|
exports.getMuniIssueByJiraKey = getMuniIssueByJiraKey;
|
|
24
|
+
// Answered = the response has text or at least one image; a cleared response object stays in the document.
|
|
25
|
+
const responseHasContent = [
|
|
26
|
+
{ "response.text": { $nin: ["", null] } },
|
|
27
|
+
{ "response.imageUrls.0": { $exists: true } },
|
|
28
|
+
];
|
|
29
|
+
// Tracked issues of one client. Issues without a status predate complaint tracking and are excluded.
|
|
30
|
+
const clientIssuesFilter = (clientId, answered) => ({
|
|
31
|
+
clientId,
|
|
32
|
+
status: { $exists: true },
|
|
33
|
+
...(answered === true && { $or: responseHasContent }),
|
|
34
|
+
...(answered === false && { $nor: responseHasContent }),
|
|
35
|
+
});
|
|
36
|
+
const findMuniIssuesByClient = async (clientId, { answered, skip = 0, limit = 0 } = {}) => {
|
|
37
|
+
return (0, exports.getMuniIssuesCollection)()
|
|
38
|
+
.find(clientIssuesFilter(clientId, answered))
|
|
39
|
+
.sort({ createdAt: -1 })
|
|
40
|
+
.skip(skip)
|
|
41
|
+
.limit(limit)
|
|
42
|
+
.toArray();
|
|
43
|
+
};
|
|
44
|
+
exports.findMuniIssuesByClient = findMuniIssuesByClient;
|
|
45
|
+
const countMuniIssuesByClient = async (clientId, { answered } = {}) => {
|
|
46
|
+
return (0, exports.getMuniIssuesCollection)().countDocuments(clientIssuesFilter(clientId, answered));
|
|
47
|
+
};
|
|
48
|
+
exports.countMuniIssuesByClient = countMuniIssuesByClient;
|
|
49
|
+
const getMuniIssueByIdForClient = async (id, clientId) => {
|
|
50
|
+
if (!mongodb_1.ObjectId.isValid(id))
|
|
51
|
+
return null;
|
|
52
|
+
return (0, exports.getMuniIssuesCollection)().findOne({
|
|
53
|
+
...clientIssuesFilter(clientId),
|
|
54
|
+
_id: new mongodb_1.ObjectId(id),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
exports.getMuniIssueByIdForClient = getMuniIssueByIdForClient;
|
|
24
58
|
//# 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,
|
|
1
|
+
{"version":3,"file":"muniIssues.getters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.getters.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
|
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;AAS1D,eAAO,MAAM,oBAAoB,EAAE,QAmElC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,gBAAgB,EAM/C,CAAC"}
|
|
@@ -52,11 +52,28 @@ 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
|
+
imageUrls: {
|
|
62
|
+
bsonType: "array",
|
|
63
|
+
maxItems: muniIssues_constants_1.MUNI_ISSUE_MAX_RESPONSE_IMAGES,
|
|
64
|
+
items: { bsonType: "string" },
|
|
65
|
+
},
|
|
66
|
+
createdAt: { bsonType: "date" },
|
|
67
|
+
updatedAt: { bsonType: "date" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
updatedAt: { bsonType: "date" },
|
|
55
71
|
},
|
|
56
72
|
};
|
|
57
73
|
exports.muniIssuesIndexes = [
|
|
58
74
|
{ key: { callSid: 1 }, name: "callSid_1" },
|
|
59
75
|
{ key: { createdAt: -1 }, name: "createdAt_-1" },
|
|
76
|
+
{ key: { clientId: 1, createdAt: -1 }, name: "clientId_1_createdAt_-1" },
|
|
60
77
|
// Unique: one stored document per Jira ticket, guarding against duplicate writes.
|
|
61
78
|
{ key: { "jira.key": 1 }, name: "jira_key_unique", unique: true },
|
|
62
79
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"muniIssues.schema.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":";;;AACA,
|
|
1
|
+
{"version":3,"file":"muniIssues.schema.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.schema.ts"],"names":[],"mappings":";;;AACA,iEAMgC;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,SAAS,EAAE;oBACT,QAAQ,EAAE,OAAO;oBACjB,QAAQ,EAAE,qDAA8B;oBACxC,KAAK,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;iBAC9B;gBACD,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;gBAC/B,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,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,6 @@
|
|
|
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
|
+
export declare const updateMuniIssueStatus: (id: string, clientId: string, status: MuniIssueStatus) => Promise<boolean>;
|
|
5
|
+
export declare const setMuniIssueResponse: (id: string, clientId: string, { status, text, imageUrls }: SetMuniIssueResponseInput) => Promise<boolean>;
|
|
4
6
|
//# 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,
|
|
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;AASF,eAAO,MAAM,qBAAqB,GAChC,IAAI,MAAM,EACV,UAAU,MAAM,EAChB,QAAQ,eAAe,KACtB,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,IAAI,MAAM,EACV,UAAU,MAAM,EAChB,6BAA6B,yBAAyB,KACrD,OAAO,CAAC,OAAO,CA2BjB,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,52 @@ const createMuniIssue = async (issueData, id) => {
|
|
|
13
15
|
...issueData.jira,
|
|
14
16
|
syncStatus: issueData.jira.syncStatus ?? "created",
|
|
15
17
|
},
|
|
16
|
-
|
|
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); answered issues stay editable.
|
|
27
|
+
const trackedIssueFilter = (id, clientId) => ({
|
|
28
|
+
_id: new mongodb_1.ObjectId(id),
|
|
29
|
+
clientId,
|
|
30
|
+
status: { $exists: true },
|
|
31
|
+
});
|
|
32
|
+
const updateMuniIssueStatus = async (id, clientId, status) => {
|
|
33
|
+
if (!mongodb_1.ObjectId.isValid(id))
|
|
34
|
+
return false;
|
|
35
|
+
await (0, muniIssues_setup_1.ensureMuniIssuesCollectionOnce)();
|
|
36
|
+
const { matchedCount } = await (0, muniIssues_getters_1.getMuniIssuesCollection)().updateOne(trackedIssueFilter(id, clientId), { $set: { status, updatedAt: new Date() } });
|
|
37
|
+
return matchedCount > 0;
|
|
38
|
+
};
|
|
39
|
+
exports.updateMuniIssueStatus = updateMuniIssueStatus;
|
|
40
|
+
const setMuniIssueResponse = async (id, clientId, { status, text, imageUrls }) => {
|
|
41
|
+
if (!mongodb_1.ObjectId.isValid(id))
|
|
42
|
+
return false;
|
|
43
|
+
await (0, muniIssues_setup_1.ensureMuniIssuesCollectionOnce)();
|
|
44
|
+
const now = new Date();
|
|
45
|
+
// Pipeline update, so the first-sent date can be read from the stored response.
|
|
46
|
+
// $literal: user text starting with "$" must not be read as a field path.
|
|
47
|
+
const { matchedCount } = await (0, muniIssues_getters_1.getMuniIssuesCollection)().updateOne(trackedIssueFilter(id, clientId), [
|
|
48
|
+
{
|
|
49
|
+
$set: {
|
|
50
|
+
status: { $literal: status },
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
response: {
|
|
53
|
+
text: { $literal: text },
|
|
54
|
+
imageUrls: { $literal: imageUrls ?? [] },
|
|
55
|
+
createdAt: { $ifNull: ["$response.createdAt", now] },
|
|
56
|
+
updatedAt: {
|
|
57
|
+
$cond: [{ $ifNull: ["$response.createdAt", false] }, now, "$$REMOVE"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
return matchedCount > 0;
|
|
64
|
+
};
|
|
65
|
+
exports.setMuniIssueResponse = setMuniIssueResponse;
|
|
22
66
|
//# sourceMappingURL=muniIssues.setters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"muniIssues.setters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"muniIssues.setters.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.setters.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
+
export type MuniIssueResponse = {
|
|
7
|
+
text: string;
|
|
8
|
+
imageUrls?: string[];
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
updatedAt?: Date;
|
|
11
|
+
};
|
|
5
12
|
export type MuniIssueContent = {
|
|
6
13
|
issuerName: string;
|
|
7
14
|
issuerRole: string;
|
|
@@ -20,11 +27,24 @@ 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
|
+
imageUrls?: string[];
|
|
44
|
+
};
|
|
45
|
+
export type FindMuniIssuesOptions = {
|
|
46
|
+
answered?: boolean;
|
|
47
|
+
skip?: number;
|
|
48
|
+
limit?: number;
|
|
49
|
+
};
|
|
30
50
|
//# 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,
|
|
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,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,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;AAKF,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,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"muniIssues.utils.d.ts","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,eAAO,MAAM,2BAA2B,GACtC,WAAW,iBAAiB,KAC3B,OACgE,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasMuniIssueResponseContent = void 0;
|
|
4
|
+
const hasMuniIssueResponseContent = (response) => Boolean(response?.text) || (response?.imageUrls?.length ?? 0) > 0;
|
|
5
|
+
exports.hasMuniIssueResponseContent = hasMuniIssueResponseContent;
|
|
6
|
+
//# sourceMappingURL=muniIssues.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"muniIssues.utils.js","sourceRoot":"","sources":["../../../src/municipal/muniIssues/muniIssues.utils.ts"],"names":[],"mappings":";;;AAEO,MAAM,2BAA2B,GAAG,CACzC,QAA4B,EACnB,EAAE,CACX,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAHvD,QAAA,2BAA2B,+BAG4B"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
import { createMuniIssue, setMuniIssueResponse } from "../muniIssues.setters";
|
|
3
|
+
import {
|
|
4
|
+
countMuniIssuesByClient,
|
|
5
|
+
findMuniIssuesByClient,
|
|
6
|
+
generateMuniIssueId,
|
|
7
|
+
getMuniIssueByIdForClient,
|
|
8
|
+
getMuniIssuesCollection,
|
|
9
|
+
} from "../muniIssues.getters";
|
|
10
|
+
import { createMuniIssueInput } from "../../../test-utils/factories";
|
|
11
|
+
|
|
12
|
+
const CLIENT = "client-a";
|
|
13
|
+
|
|
14
|
+
const createIssue = async (clientId = CLIENT) =>
|
|
15
|
+
(await createMuniIssue(createMuniIssueInput({ clientId }), generateMuniIssueId())).toString();
|
|
16
|
+
|
|
17
|
+
describe("muniIssues getters (complaint tracking)", () => {
|
|
18
|
+
describe("findMuniIssuesByClient / countMuniIssuesByClient", () => {
|
|
19
|
+
it("should return only the client's tracked issues, newest first", async () => {
|
|
20
|
+
const older = await createIssue();
|
|
21
|
+
const newer = await createIssue();
|
|
22
|
+
// Both can land in the same millisecond; make the order explicit.
|
|
23
|
+
await getMuniIssuesCollection().updateOne(
|
|
24
|
+
{ _id: new ObjectId(older) },
|
|
25
|
+
{ $set: { createdAt: new Date(Date.now() - 60_000) } },
|
|
26
|
+
);
|
|
27
|
+
await createIssue("client-b");
|
|
28
|
+
// An issue created before complaint tracking has no status.
|
|
29
|
+
await getMuniIssuesCollection().insertOne({
|
|
30
|
+
...createMuniIssueInput({ clientId: CLIENT }),
|
|
31
|
+
_id: generateMuniIssueId(),
|
|
32
|
+
jira: { key: "CP-legacy", syncStatus: "created" },
|
|
33
|
+
createdAt: new Date(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const issues = await findMuniIssuesByClient(CLIENT);
|
|
37
|
+
|
|
38
|
+
expect(issues.map((i) => i._id.toString())).toEqual([newer, older]);
|
|
39
|
+
expect(await countMuniIssuesByClient(CLIENT)).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should filter by answered and apply skip/limit", async () => {
|
|
43
|
+
const answered = await createIssue();
|
|
44
|
+
await createIssue();
|
|
45
|
+
await createIssue();
|
|
46
|
+
await setMuniIssueResponse(answered, CLIENT, { status: "resolved", text: "Done" });
|
|
47
|
+
|
|
48
|
+
expect(await countMuniIssuesByClient(CLIENT, { answered: true })).toBe(1);
|
|
49
|
+
expect(await countMuniIssuesByClient(CLIENT, { answered: false })).toBe(2);
|
|
50
|
+
expect(
|
|
51
|
+
(await findMuniIssuesByClient(CLIENT, { answered: true }))[0]._id.toString(),
|
|
52
|
+
).toBe(answered);
|
|
53
|
+
expect(await findMuniIssuesByClient(CLIENT, { skip: 1, limit: 1 })).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should treat a response with only images as answered and a cleared response as waiting", async () => {
|
|
57
|
+
const imagesOnly = await createIssue();
|
|
58
|
+
const cleared = await createIssue();
|
|
59
|
+
const textOnly = await createIssue();
|
|
60
|
+
await setMuniIssueResponse(imagesOnly, CLIENT, {
|
|
61
|
+
status: "resolved",
|
|
62
|
+
text: "",
|
|
63
|
+
imageUrls: ["https://example.com/a.png"],
|
|
64
|
+
});
|
|
65
|
+
await setMuniIssueResponse(cleared, CLIENT, { status: "resolved", text: "Done" });
|
|
66
|
+
await setMuniIssueResponse(cleared, CLIENT, { status: "resolved", text: "", imageUrls: [] });
|
|
67
|
+
await setMuniIssueResponse(textOnly, CLIENT, { status: "resolved", text: "Done" });
|
|
68
|
+
|
|
69
|
+
const answered = await findMuniIssuesByClient(CLIENT, { answered: true });
|
|
70
|
+
const waiting = await findMuniIssuesByClient(CLIENT, { answered: false });
|
|
71
|
+
|
|
72
|
+
expect(answered.map((i) => i._id.toString()).sort()).toEqual([imagesOnly, textOnly].sort());
|
|
73
|
+
expect(waiting.map((i) => i._id.toString())).toEqual([cleared]);
|
|
74
|
+
expect(await countMuniIssuesByClient(CLIENT, { answered: false })).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("getMuniIssueByIdForClient", () => {
|
|
79
|
+
it("should return the issue only for its own client", async () => {
|
|
80
|
+
const id = await createIssue();
|
|
81
|
+
|
|
82
|
+
expect((await getMuniIssueByIdForClient(id, CLIENT))?._id.toString()).toBe(id);
|
|
83
|
+
expect(await getMuniIssueByIdForClient(id, "client-b")).toBeNull();
|
|
84
|
+
expect(await getMuniIssueByIdForClient("not-a-valid-id", CLIENT)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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,113 @@ 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
|
+
imageUrls: ["https://example.com/a.png", "https://example.com/b.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
|
+
imageUrls: ["https://example.com/a.png", "https://example.com/b.png"],
|
|
148
|
+
});
|
|
149
|
+
expect(issue?.response?.createdAt).toBeInstanceOf(Date);
|
|
150
|
+
expect(issue?.response?.updatedAt).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should let an answered issue be edited, keeping the first-sent date", async () => {
|
|
154
|
+
const input = createMuniIssueInput();
|
|
155
|
+
const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
|
|
156
|
+
await setMuniIssueResponse(id, input.clientId, {
|
|
157
|
+
status: "in_review",
|
|
158
|
+
text: "First",
|
|
159
|
+
imageUrls: ["https://example.com/a.png"],
|
|
160
|
+
});
|
|
161
|
+
const firstSentAt = (await getMuniIssueById(id))?.response?.createdAt;
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
await setMuniIssueResponse(id, input.clientId, { status: "resolved", text: "Second" }),
|
|
165
|
+
).toBe(true);
|
|
166
|
+
expect(await updateMuniIssueStatus(id, input.clientId, "no_issue_found")).toBe(true);
|
|
167
|
+
|
|
168
|
+
const issue = await getMuniIssueById(id);
|
|
169
|
+
expect(issue?.status).toBe("no_issue_found");
|
|
170
|
+
expect(issue?.response?.text).toBe("Second");
|
|
171
|
+
expect(issue?.response?.imageUrls).toEqual([]);
|
|
172
|
+
expect(issue?.response?.createdAt).toEqual(firstSentAt);
|
|
173
|
+
expect(issue?.response?.updatedAt).toBeInstanceOf(Date);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should save images without text, and keep a cleared response with its first-sent date", async () => {
|
|
177
|
+
const input = createMuniIssueInput();
|
|
178
|
+
const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
|
|
179
|
+
|
|
180
|
+
await setMuniIssueResponse(id, input.clientId, {
|
|
181
|
+
status: "resolved",
|
|
182
|
+
text: "",
|
|
183
|
+
imageUrls: ["https://example.com/a.png"],
|
|
184
|
+
});
|
|
185
|
+
const issueWithImage = await getMuniIssueById(id);
|
|
186
|
+
await setMuniIssueResponse(id, input.clientId, { status: "resolved", text: "", imageUrls: [] });
|
|
187
|
+
|
|
188
|
+
const response = (await getMuniIssueById(id))?.response;
|
|
189
|
+
expect(issueWithImage?.response?.imageUrls).toEqual(["https://example.com/a.png"]);
|
|
190
|
+
expect(response?.text).toBe("");
|
|
191
|
+
expect(response?.imageUrls).toEqual([]);
|
|
192
|
+
expect(response?.createdAt).toEqual(issueWithImage?.response?.createdAt);
|
|
193
|
+
expect(response?.updatedAt).toBeInstanceOf(Date);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should store text starting with $ as plain text", async () => {
|
|
197
|
+
const input = createMuniIssueInput();
|
|
198
|
+
const id = (await createMuniIssue(input, generateMuniIssueId())).toString();
|
|
199
|
+
|
|
200
|
+
await setMuniIssueResponse(id, input.clientId, { status: "resolved", text: "$status" });
|
|
201
|
+
|
|
202
|
+
expect((await getMuniIssueById(id))?.response?.text).toBe("$status");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should not touch another client's issue or a malformed id", async () => {
|
|
206
|
+
const id = (
|
|
207
|
+
await createMuniIssue(createMuniIssueInput(), generateMuniIssueId())
|
|
208
|
+
).toString();
|
|
209
|
+
|
|
210
|
+
expect(
|
|
211
|
+
await setMuniIssueResponse(id, "other-client", { status: "resolved", text: "X" }),
|
|
212
|
+
).toBe(false);
|
|
213
|
+
expect(
|
|
214
|
+
await setMuniIssueResponse("not-a-valid-id", "any", { status: "resolved", text: "X" }),
|
|
215
|
+
).toBe(false);
|
|
216
|
+
expect((await getMuniIssueById(id))?.response).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
94
219
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { hasMuniIssueResponseContent } from "../muniIssues.utils";
|
|
2
|
+
|
|
3
|
+
describe("hasMuniIssueResponseContent", () => {
|
|
4
|
+
const base = { createdAt: new Date() };
|
|
5
|
+
|
|
6
|
+
it("should be true for text, images, or both", () => {
|
|
7
|
+
expect(hasMuniIssueResponseContent({ ...base, text: "Done" })).toBe(true);
|
|
8
|
+
expect(hasMuniIssueResponseContent({ ...base, text: "", imageUrls: ["a"] })).toBe(true);
|
|
9
|
+
expect(hasMuniIssueResponseContent({ ...base, text: "Done", imageUrls: ["a"] })).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should be false for no response or an empty one", () => {
|
|
13
|
+
expect(hasMuniIssueResponseContent(undefined)).toBe(false);
|
|
14
|
+
expect(hasMuniIssueResponseContent({ ...base, text: "" })).toBe(false);
|
|
15
|
+
expect(hasMuniIssueResponseContent({ ...base, text: "", imageUrls: [] })).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./muniIssues.getters";
|
|
2
2
|
export * from "./muniIssues.setters";
|
|
3
3
|
export * from "./muniIssues.setup";
|
|
4
|
+
export * from "./muniIssues.utils";
|
|
4
5
|
export {
|
|
5
6
|
MUNI_ISSUES_COLLECTION,
|
|
7
|
+
MUNI_ISSUE_MAX_RESPONSE_IMAGES,
|
|
8
|
+
MUNI_ISSUE_STATUSES,
|
|
6
9
|
MUNI_ISSUE_SYNC_STATUSES,
|
|
7
10
|
MUNI_ISSUE_TEXT_MAX_LENGTH,
|
|
8
11
|
MUNI_ISSUE_NAME_MAX_LENGTH,
|
|
@@ -13,5 +16,9 @@ export type {
|
|
|
13
16
|
MuniIssueContent,
|
|
14
17
|
MuniIssueJira,
|
|
15
18
|
MuniIssueSyncStatus,
|
|
19
|
+
MuniIssueStatus,
|
|
20
|
+
MuniIssueResponse,
|
|
16
21
|
CreateMuniIssueInput,
|
|
22
|
+
SetMuniIssueResponseInput,
|
|
23
|
+
FindMuniIssuesOptions,
|
|
17
24
|
} from "./muniIssues.types";
|
|
@@ -4,6 +4,15 @@ 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
|
+
export const MUNI_ISSUE_STATUSES = [
|
|
8
|
+
"in_review",
|
|
9
|
+
"in_progress",
|
|
10
|
+
"resolved",
|
|
11
|
+
"no_issue_found",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const MUNI_ISSUE_MAX_RESPONSE_IMAGES = 5;
|
|
15
|
+
|
|
7
16
|
/** Max length for the long free-text fields — well above the ~2000-char target. */
|
|
8
17
|
export const MUNI_ISSUE_TEXT_MAX_LENGTH = 5000;
|
|
9
18
|
|
|
@@ -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,52 @@ export const getMuniIssueByJiraKey = async (
|
|
|
22
22
|
): Promise<MuniIssue | null> => {
|
|
23
23
|
return getMuniIssuesCollection().findOne({ "jira.key": jiraKey });
|
|
24
24
|
};
|
|
25
|
+
|
|
26
|
+
// Answered = the response has text or at least one image; a cleared response object stays in the document.
|
|
27
|
+
const responseHasContent: Filter<MuniIssue>[] = [
|
|
28
|
+
{ "response.text": { $nin: ["", null] } },
|
|
29
|
+
{ "response.imageUrls.0": { $exists: true } },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Tracked issues of one client. Issues without a status predate complaint tracking and are excluded.
|
|
33
|
+
const clientIssuesFilter = (
|
|
34
|
+
clientId: string,
|
|
35
|
+
answered?: boolean,
|
|
36
|
+
): Filter<MuniIssue> => ({
|
|
37
|
+
clientId,
|
|
38
|
+
status: { $exists: true },
|
|
39
|
+
...(answered === true && { $or: responseHasContent }),
|
|
40
|
+
...(answered === false && { $nor: responseHasContent }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const findMuniIssuesByClient = async (
|
|
44
|
+
clientId: string,
|
|
45
|
+
{ answered, skip = 0, limit = 0 }: FindMuniIssuesOptions = {},
|
|
46
|
+
): Promise<MuniIssue[]> => {
|
|
47
|
+
return getMuniIssuesCollection()
|
|
48
|
+
.find(clientIssuesFilter(clientId, answered))
|
|
49
|
+
.sort({ createdAt: -1 })
|
|
50
|
+
.skip(skip)
|
|
51
|
+
.limit(limit)
|
|
52
|
+
.toArray();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const countMuniIssuesByClient = async (
|
|
56
|
+
clientId: string,
|
|
57
|
+
{ answered }: Pick<FindMuniIssuesOptions, "answered"> = {},
|
|
58
|
+
): Promise<number> => {
|
|
59
|
+
return getMuniIssuesCollection().countDocuments(
|
|
60
|
+
clientIssuesFilter(clientId, answered),
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const getMuniIssueByIdForClient = async (
|
|
65
|
+
id: string,
|
|
66
|
+
clientId: string,
|
|
67
|
+
): Promise<MuniIssue | null> => {
|
|
68
|
+
if (!ObjectId.isValid(id)) return null;
|
|
69
|
+
return getMuniIssuesCollection().findOne({
|
|
70
|
+
...clientIssuesFilter(clientId),
|
|
71
|
+
_id: new ObjectId(id),
|
|
72
|
+
});
|
|
73
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Document, IndexDescription } from "mongodb";
|
|
2
2
|
import {
|
|
3
|
+
MUNI_ISSUE_MAX_RESPONSE_IMAGES,
|
|
4
|
+
MUNI_ISSUE_STATUSES,
|
|
3
5
|
MUNI_ISSUE_SYNC_STATUSES,
|
|
4
6
|
MUNI_ISSUE_TEXT_MAX_LENGTH,
|
|
5
7
|
MUNI_ISSUE_NAME_MAX_LENGTH,
|
|
@@ -55,12 +57,29 @@ export const muniIssuesJsonSchema: Document = {
|
|
|
55
57
|
},
|
|
56
58
|
},
|
|
57
59
|
createdAt: { bsonType: "date" },
|
|
60
|
+
status: { enum: [...MUNI_ISSUE_STATUSES] },
|
|
61
|
+
response: {
|
|
62
|
+
bsonType: "object",
|
|
63
|
+
required: ["text", "createdAt"],
|
|
64
|
+
properties: {
|
|
65
|
+
text: { bsonType: "string", maxLength: MUNI_ISSUE_TEXT_MAX_LENGTH },
|
|
66
|
+
imageUrls: {
|
|
67
|
+
bsonType: "array",
|
|
68
|
+
maxItems: MUNI_ISSUE_MAX_RESPONSE_IMAGES,
|
|
69
|
+
items: { bsonType: "string" },
|
|
70
|
+
},
|
|
71
|
+
createdAt: { bsonType: "date" },
|
|
72
|
+
updatedAt: { bsonType: "date" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
updatedAt: { bsonType: "date" },
|
|
58
76
|
},
|
|
59
77
|
};
|
|
60
78
|
|
|
61
79
|
export const muniIssuesIndexes: IndexDescription[] = [
|
|
62
80
|
{ key: { callSid: 1 }, name: "callSid_1" },
|
|
63
81
|
{ key: { createdAt: -1 }, name: "createdAt_-1" },
|
|
82
|
+
{ key: { clientId: 1, createdAt: -1 }, name: "clientId_1_createdAt_-1" },
|
|
64
83
|
// Unique: one stored document per Jira ticket, guarding against duplicate writes.
|
|
65
84
|
{ key: { "jira.key": 1 }, name: "jira_key_unique", unique: true },
|
|
66
85
|
];
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { ObjectId } from "mongodb";
|
|
2
|
-
import {
|
|
1
|
+
import { Filter, ObjectId } from "mongodb";
|
|
2
|
+
import {
|
|
3
|
+
CreateMuniIssueInput,
|
|
4
|
+
MuniIssue,
|
|
5
|
+
MuniIssueStatus,
|
|
6
|
+
SetMuniIssueResponseInput,
|
|
7
|
+
} from "./muniIssues.types";
|
|
3
8
|
import { getMuniIssuesCollection } from "./muniIssues.getters";
|
|
4
9
|
import { ensureMuniIssuesCollectionOnce } from "./muniIssues.setup";
|
|
5
10
|
|
|
@@ -10,6 +15,7 @@ export const createMuniIssue = async (
|
|
|
10
15
|
): Promise<ObjectId> => {
|
|
11
16
|
await ensureMuniIssuesCollectionOnce();
|
|
12
17
|
|
|
18
|
+
const now = new Date();
|
|
13
19
|
const issue: MuniIssue = {
|
|
14
20
|
_id: id,
|
|
15
21
|
...issueData,
|
|
@@ -17,8 +23,65 @@ export const createMuniIssue = async (
|
|
|
17
23
|
...issueData.jira,
|
|
18
24
|
syncStatus: issueData.jira.syncStatus ?? "created",
|
|
19
25
|
},
|
|
20
|
-
|
|
26
|
+
status: "in_review",
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
21
29
|
};
|
|
22
30
|
const { insertedId } = await getMuniIssuesCollection().insertOne(issue);
|
|
23
31
|
return insertedId;
|
|
24
32
|
};
|
|
33
|
+
|
|
34
|
+
// An issue of this client that is tracked (has a status); answered issues stay editable.
|
|
35
|
+
const trackedIssueFilter = (id: string, clientId: string): Filter<MuniIssue> => ({
|
|
36
|
+
_id: new ObjectId(id),
|
|
37
|
+
clientId,
|
|
38
|
+
status: { $exists: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const updateMuniIssueStatus = async (
|
|
42
|
+
id: string,
|
|
43
|
+
clientId: string,
|
|
44
|
+
status: MuniIssueStatus,
|
|
45
|
+
): Promise<boolean> => {
|
|
46
|
+
if (!ObjectId.isValid(id)) return false;
|
|
47
|
+
await ensureMuniIssuesCollectionOnce();
|
|
48
|
+
|
|
49
|
+
const { matchedCount } = await getMuniIssuesCollection().updateOne(
|
|
50
|
+
trackedIssueFilter(id, clientId),
|
|
51
|
+
{ $set: { status, updatedAt: new Date() } },
|
|
52
|
+
);
|
|
53
|
+
return matchedCount > 0;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const setMuniIssueResponse = async (
|
|
57
|
+
id: string,
|
|
58
|
+
clientId: string,
|
|
59
|
+
{ status, text, imageUrls }: SetMuniIssueResponseInput,
|
|
60
|
+
): Promise<boolean> => {
|
|
61
|
+
if (!ObjectId.isValid(id)) return false;
|
|
62
|
+
await ensureMuniIssuesCollectionOnce();
|
|
63
|
+
|
|
64
|
+
const now = new Date();
|
|
65
|
+
// Pipeline update, so the first-sent date can be read from the stored response.
|
|
66
|
+
// $literal: user text starting with "$" must not be read as a field path.
|
|
67
|
+
const { matchedCount } = await getMuniIssuesCollection().updateOne(
|
|
68
|
+
trackedIssueFilter(id, clientId),
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
$set: {
|
|
72
|
+
status: { $literal: status },
|
|
73
|
+
updatedAt: now,
|
|
74
|
+
response: {
|
|
75
|
+
text: { $literal: text },
|
|
76
|
+
imageUrls: { $literal: imageUrls ?? [] },
|
|
77
|
+
createdAt: { $ifNull: ["$response.createdAt", now] },
|
|
78
|
+
updatedAt: {
|
|
79
|
+
$cond: [{ $ifNull: ["$response.createdAt", false] }, now, "$$REMOVE"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
);
|
|
86
|
+
return matchedCount > 0;
|
|
87
|
+
};
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { ObjectId } from "mongodb";
|
|
2
2
|
|
|
3
3
|
import { CityName } from "../utils/types";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MUNI_ISSUE_STATUSES,
|
|
6
|
+
MUNI_ISSUE_SYNC_STATUSES,
|
|
7
|
+
} from "./muniIssues.constants";
|
|
5
8
|
|
|
6
9
|
export type MuniIssueSyncStatus = (typeof MUNI_ISSUE_SYNC_STATUSES)[number];
|
|
7
10
|
|
|
11
|
+
export type MuniIssueStatus = (typeof MUNI_ISSUE_STATUSES)[number];
|
|
12
|
+
|
|
13
|
+
export type MuniIssueResponse = {
|
|
14
|
+
text: string;
|
|
15
|
+
imageUrls?: string[];
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
updatedAt?: Date;
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
export type MuniIssueContent = {
|
|
9
21
|
issuerName: string;
|
|
10
22
|
issuerRole: string;
|
|
@@ -25,13 +37,31 @@ export type MuniIssue = {
|
|
|
25
37
|
issueContent: MuniIssueContent;
|
|
26
38
|
jira: MuniIssueJira;
|
|
27
39
|
createdAt: Date;
|
|
40
|
+
// Complaint tracking (CP-1888). Optional: issues created before it have none
|
|
41
|
+
// of these, which is how they are kept out of tracking.
|
|
42
|
+
status?: MuniIssueStatus;
|
|
43
|
+
response?: MuniIssueResponse;
|
|
44
|
+
updatedAt?: Date;
|
|
28
45
|
};
|
|
29
46
|
|
|
30
|
-
// _id/createdAt are set on insert
|
|
31
|
-
// because the issue is only stored after its Jira
|
|
47
|
+
// _id/createdAt/status/updatedAt are set on insert, response only later (setMuniIssueResponse);
|
|
48
|
+
// jira.syncStatus defaults to "created" because the issue is only stored after its Jira
|
|
49
|
+
// ticket exists (CP-1115).
|
|
32
50
|
export type CreateMuniIssueInput = Omit<
|
|
33
51
|
MuniIssue,
|
|
34
|
-
"_id" | "createdAt" | "jira"
|
|
52
|
+
"_id" | "createdAt" | "jira" | "status" | "response" | "updatedAt"
|
|
35
53
|
> & {
|
|
36
54
|
jira: { key: string; syncStatus?: MuniIssueSyncStatus };
|
|
37
55
|
};
|
|
56
|
+
|
|
57
|
+
export type SetMuniIssueResponseInput = {
|
|
58
|
+
status: MuniIssueStatus;
|
|
59
|
+
text: string;
|
|
60
|
+
imageUrls?: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type FindMuniIssuesOptions = {
|
|
64
|
+
answered?: boolean;
|
|
65
|
+
skip?: number;
|
|
66
|
+
limit?: number;
|
|
67
|
+
};
|