orbitx-core-models 11.174.0 → 11.176.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/helpers/_http.d.ts +10 -0
  2. package/dist/helpers/_http.d.ts.map +1 -0
  3. package/dist/helpers/_http.js +16 -0
  4. package/dist/helpers/addonState.d.ts +20 -0
  5. package/dist/helpers/addonState.d.ts.map +1 -0
  6. package/dist/helpers/addonState.js +77 -0
  7. package/dist/helpers/analyticsBuckets.d.ts +32 -0
  8. package/dist/helpers/analyticsBuckets.d.ts.map +1 -0
  9. package/dist/helpers/analyticsBuckets.js +143 -0
  10. package/dist/helpers/avatars.d.ts +15 -0
  11. package/dist/helpers/avatars.d.ts.map +1 -0
  12. package/dist/helpers/avatars.js +122 -0
  13. package/dist/helpers/chatAttachments.d.ts +33 -0
  14. package/dist/helpers/chatAttachments.d.ts.map +1 -0
  15. package/dist/helpers/chatAttachments.js +81 -0
  16. package/dist/helpers/contactDecrypt.d.ts +26 -0
  17. package/dist/helpers/contactDecrypt.d.ts.map +1 -0
  18. package/dist/helpers/contactDecrypt.js +95 -0
  19. package/dist/helpers/emailTemplates.d.ts +19 -0
  20. package/dist/helpers/emailTemplates.d.ts.map +1 -0
  21. package/dist/helpers/emailTemplates.js +276 -0
  22. package/dist/helpers/errors.d.ts +27 -0
  23. package/dist/helpers/errors.d.ts.map +1 -0
  24. package/dist/helpers/errors.js +101 -0
  25. package/dist/helpers/invoiceDocument.d.ts +63 -0
  26. package/dist/helpers/invoiceDocument.d.ts.map +1 -0
  27. package/dist/helpers/invoiceDocument.js +148 -0
  28. package/dist/helpers/listQuery.d.ts +35 -0
  29. package/dist/helpers/listQuery.d.ts.map +1 -0
  30. package/dist/helpers/listQuery.js +180 -0
  31. package/dist/helpers/myUpdates.d.ts +16 -0
  32. package/dist/helpers/myUpdates.d.ts.map +1 -0
  33. package/dist/helpers/myUpdates.js +94 -0
  34. package/dist/helpers/ownActivity.d.ts +8 -0
  35. package/dist/helpers/ownActivity.d.ts.map +1 -0
  36. package/dist/helpers/ownActivity.js +114 -0
  37. package/dist/helpers/ownTickets.d.ts +21 -0
  38. package/dist/helpers/ownTickets.d.ts.map +1 -0
  39. package/dist/helpers/ownTickets.js +169 -0
  40. package/dist/helpers/s3Storage.d.ts +38 -0
  41. package/dist/helpers/s3Storage.d.ts.map +1 -0
  42. package/dist/helpers/s3Storage.js +73 -0
  43. package/dist/helpers/sessions.d.ts +22 -0
  44. package/dist/helpers/sessions.d.ts.map +1 -0
  45. package/dist/helpers/sessions.js +61 -0
  46. package/dist/index.d.ts +14 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +29 -0
  49. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ /** Runs an async route handler and hands any error to Express's error handler. */
2
+ export declare const catchAsync: (fn: (req: any, res: any, next: any) => Promise<any>) => (req: any, res: any, next: any) => void;
3
+ export declare const HTTP: {
4
+ readonly OK: 200;
5
+ readonly CREATED: 201;
6
+ readonly BAD_REQUEST: 400;
7
+ readonly FORBIDDEN: 403;
8
+ readonly NOT_FOUND: 404;
9
+ };
10
+ //# sourceMappingURL=_http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_http.d.ts","sourceRoot":"","sources":["../../src/helpers/_http.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,eAAO,MAAM,UAAU,GACpB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,MACnD,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,SAE7B,CAAC;AAEJ,eAAO,MAAM,IAAI;;;;;;CAMP,CAAC"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // Internal to the helpers (not exported from the package): Express plumbing the shared controllers use.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.HTTP = exports.catchAsync = void 0;
5
+ /** Runs an async route handler and hands any error to Express's error handler. */
6
+ const catchAsync = (fn) => (req, res, next) => {
7
+ fn(req, res, next).catch(next);
8
+ };
9
+ exports.catchAsync = catchAsync;
10
+ exports.HTTP = {
11
+ OK: 200,
12
+ CREATED: 201,
13
+ BAD_REQUEST: 400,
14
+ FORBIDDEN: 403,
15
+ NOT_FOUND: 404,
16
+ };
@@ -0,0 +1,20 @@
1
+ export declare const normalizeAddonSlug: (s: unknown) => string;
2
+ /**
3
+ * Which add-ons a company owns and which of those Ops has paused. Paused add-ons stay visible in every dashboard
4
+ * but their runtime behaviour must stop. Returns { owned, paused } as sets of normalised slugs/names.
5
+ */
6
+ export declare function getAddonState(companyId: unknown): Promise<{
7
+ owned: Set<string>;
8
+ paused: Set<string>;
9
+ }>;
10
+ /**
11
+ * True when the company owns at least one of `slugs` and every owned one is paused. Fails open (false) on any
12
+ * error so a lookup problem can never break live chats.
13
+ */
14
+ export declare function isAddonPaused(companyId: unknown, slugs: string[]): Promise<boolean>;
15
+ /**
16
+ * True when the company has NOT bought any of `slugs`, or every one it bought is paused, i.e. the feature must not
17
+ * run. Fails open (false) on error so a lookup problem never silently stops leads reaching a client's CRM.
18
+ */
19
+ export declare function isAddonUnavailable(companyId: unknown, slugs: string[]): Promise<boolean>;
20
+ //# sourceMappingURL=addonState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addonState.d.ts","sourceRoot":"","sources":["../../src/helpers/addonState.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,kBAAkB,GAAI,GAAG,OAAO,KAAG,MAKtB,CAAC;AAI3B;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAuB5G;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAUzF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9F"}
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeAddonSlug = void 0;
4
+ exports.getAddonState = getAddonState;
5
+ exports.isAddonPaused = isAddonPaused;
6
+ exports.isAddonUnavailable = isAddonUnavailable;
7
+ const subscription_model_1 = require("../models/subscription.model");
8
+ // Slugs are typed by hand in Ops (some use hyphens, some underscores), so compare a normalised form of both the
9
+ // slug and the name.
10
+ const normalizeAddonSlug = (s) => String(s || "")
11
+ .trim()
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]+/g, "-")
14
+ .replace(/^-|-$/g, "");
15
+ exports.normalizeAddonSlug = normalizeAddonSlug;
16
+ const norm = exports.normalizeAddonSlug;
17
+ /**
18
+ * Which add-ons a company owns and which of those Ops has paused. Paused add-ons stay visible in every dashboard
19
+ * but their runtime behaviour must stop. Returns { owned, paused } as sets of normalised slugs/names.
20
+ */
21
+ async function getAddonState(companyId) {
22
+ const owned = new Set();
23
+ const paused = new Set();
24
+ if (!companyId)
25
+ return { owned, paused };
26
+ const sub = await subscription_model_1.Subscription.findOne({ company: companyId })
27
+ .select("addons disabledAddons")
28
+ .populate("addons.addon", "slug name")
29
+ .lean();
30
+ if (!sub)
31
+ return { owned, paused };
32
+ const disabled = new Set((sub.disabledAddons || []).map(String));
33
+ for (const row of sub.addons || []) {
34
+ const addon = row.addon;
35
+ if (!addon)
36
+ continue;
37
+ const keys = [norm(addon.slug), norm(addon.name)].filter(Boolean);
38
+ const isPaused = disabled.has(String(addon._id));
39
+ for (const k of keys) {
40
+ owned.add(k);
41
+ if (isPaused)
42
+ paused.add(k);
43
+ }
44
+ }
45
+ return { owned, paused };
46
+ }
47
+ /**
48
+ * True when the company owns at least one of `slugs` and every owned one is paused. Fails open (false) on any
49
+ * error so a lookup problem can never break live chats.
50
+ */
51
+ async function isAddonPaused(companyId, slugs) {
52
+ try {
53
+ const { owned, paused } = await getAddonState(companyId);
54
+ const wanted = slugs.map(norm);
55
+ const ownedWanted = wanted.filter((s) => owned.has(s));
56
+ return ownedWanted.length > 0 && ownedWanted.every((s) => paused.has(s));
57
+ }
58
+ catch (err) {
59
+ console.error("[addonState] lookup failed, treating add-on as active:", err === null || err === void 0 ? void 0 : err.message);
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * True when the company has NOT bought any of `slugs`, or every one it bought is paused, i.e. the feature must not
65
+ * run. Fails open (false) on error so a lookup problem never silently stops leads reaching a client's CRM.
66
+ */
67
+ async function isAddonUnavailable(companyId, slugs) {
68
+ try {
69
+ const { owned, paused } = await getAddonState(companyId);
70
+ const active = slugs.map(norm).filter((s) => owned.has(s) && !paused.has(s));
71
+ return active.length === 0;
72
+ }
73
+ catch (err) {
74
+ console.error("[addonState] lookup failed, treating add-on as available:", err === null || err === void 0 ? void 0 : err.message);
75
+ return false;
76
+ }
77
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Time buckets for the analytics charts (Ops and Client). The bucket size follows the selected date range (hourly
3
+ * for a day, daily for a week or month, weekly for a few months, monthly for a year) unless the caller asks for a
4
+ * specific one. Every bucket in the range is returned, so quiet periods show as zero instead of disappearing, and
5
+ * the keys built here match the keys MongoDB builds in groupKeyExpr.
6
+ */
7
+ export declare const UNITS: readonly ["hour", "day", "week", "month"];
8
+ export type BucketUnit = (typeof UNITS)[number];
9
+ type Range = {
10
+ $gte?: unknown;
11
+ $lte?: unknown;
12
+ } | null | undefined;
13
+ export declare const safeTimezone: (tz: unknown) => string;
14
+ /** The bucket size to use: the requested one when it fits the range, otherwise one that does. */
15
+ export declare const pickUnit: (range: Range, requested?: unknown, now?: Date) => BucketUnit;
16
+ /** The bucket a moment falls in, as a string key. */
17
+ export declare const keyOf: (date: Date, unit: BucketUnit, tz: string) => string;
18
+ /** Every bucket key between the range's start and end, in order. */
19
+ export declare const bucketKeys: (range: Range, unit: BucketUnit, tz: string, now?: Date) => string[];
20
+ /** A short axis label for a bucket key. */
21
+ export declare const labelOf: (key: string, unit: BucketUnit, multiYear?: boolean) => string;
22
+ /** A MongoDB expression that yields the same bucket key keyOf produces, for a date field. */
23
+ export declare const groupKeyExpr: (unit: BucketUnit, tz: string, field?: string) => Record<string, unknown>;
24
+ /** Bucket keys, labels and the unit used, for a range. */
25
+ export declare const buildAxis: (range: Range, requested?: unknown, tzInput?: unknown, now?: Date) => {
26
+ tz: string;
27
+ unit: "day" | "month" | "hour" | "week";
28
+ keys: string[];
29
+ labels: string[];
30
+ };
31
+ export {};
32
+ //# sourceMappingURL=analyticsBuckets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyticsBuckets.d.ts","sourceRoot":"","sources":["../../src/helpers/analyticsBuckets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,KAAK,2CAA4C,CAAC;AAC/D,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAChD,KAAK,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC;AAOnE,eAAO,MAAM,YAAY,GAAI,IAAI,OAAO,KAAG,MAQ1C,CAAC;AAQF,iGAAiG;AACjG,eAAO,MAAM,QAAQ,GAAI,OAAO,KAAK,EAAE,YAAY,OAAO,EAAE,UAAgB,KAAG,UAgB9E,CAAC;AAiBF,qDAAqD;AACrD,eAAO,MAAM,KAAK,GAAI,MAAM,IAAI,EAAE,MAAM,UAAU,EAAE,IAAI,MAAM,KAAG,MAUhE,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,UAAU,GAAI,OAAO,KAAK,EAAE,MAAM,UAAU,EAAE,IAAI,MAAM,EAAE,UAAgB,KAAG,MAAM,EAgB/F,CAAC;AAIF,2CAA2C;AAC3C,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,MAAM,UAAU,EAAE,mBAAiB,KAAG,MAK1E,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,YAAY,GAAI,MAAM,UAAU,EAAE,IAAI,MAAM,EAAE,cAAoB,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAavG,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,SAAS,GAAI,OAAO,KAAK,EAAE,YAAY,OAAO,EAAE,UAAU,OAAO,EAAE,UAAgB;;;;;CAM/F,CAAC"}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Time buckets for the analytics charts (Ops and Client). The bucket size follows the selected date range (hourly
4
+ * for a day, daily for a week or month, weekly for a few months, monthly for a year) unless the caller asks for a
5
+ * specific one. Every bucket in the range is returned, so quiet periods show as zero instead of disappearing, and
6
+ * the keys built here match the keys MongoDB builds in groupKeyExpr.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.buildAxis = exports.groupKeyExpr = exports.labelOf = exports.bucketKeys = exports.keyOf = exports.pickUnit = exports.safeTimezone = exports.UNITS = void 0;
10
+ exports.UNITS = ["hour", "day", "week", "month"];
11
+ const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
12
+ const HOUR_MS = 3600 * 1000;
13
+ const DAY_MS = 24 * HOUR_MS;
14
+ const MAX_BUCKETS = 400;
15
+ const safeTimezone = (tz) => {
16
+ if (!tz || typeof tz !== "string")
17
+ return "UTC";
18
+ try {
19
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
20
+ return tz;
21
+ }
22
+ catch {
23
+ return "UTC";
24
+ }
25
+ };
26
+ exports.safeTimezone = safeTimezone;
27
+ const rangeBounds = (range, now = new Date()) => {
28
+ const end = range && range.$lte ? new Date(range.$lte) : now;
29
+ const start = range && range.$gte ? new Date(range.$gte) : new Date(end.getTime() - 30 * DAY_MS);
30
+ return { start, end };
31
+ };
32
+ /** The bucket size to use: the requested one when it fits the range, otherwise one that does. */
33
+ const pickUnit = (range, requested, now = new Date()) => {
34
+ const { start, end } = rangeBounds(range, now);
35
+ const spanDays = Math.max(0, (end.getTime() - start.getTime()) / DAY_MS);
36
+ let unit = exports.UNITS.includes(requested) ? requested : null;
37
+ if (!unit) {
38
+ if (spanDays <= 1.5)
39
+ unit = "hour";
40
+ else if (spanDays <= 62)
41
+ unit = "day";
42
+ else if (spanDays <= 190)
43
+ unit = "week";
44
+ else
45
+ unit = "month";
46
+ }
47
+ // Never return so many buckets that the chart becomes unreadable.
48
+ if (unit === "hour" && spanDays > 4)
49
+ unit = "day";
50
+ if (unit === "day" && spanDays > 200)
51
+ unit = "week";
52
+ if (unit === "week" && spanDays > 1100)
53
+ unit = "month";
54
+ return unit;
55
+ };
56
+ exports.pickUnit = pickUnit;
57
+ const pad = (n) => String(n).padStart(2, "0");
58
+ const localParts = (date, tz) => {
59
+ const parts = new Intl.DateTimeFormat("en-CA", {
60
+ timeZone: tz,
61
+ hourCycle: "h23",
62
+ year: "numeric",
63
+ month: "2-digit",
64
+ day: "2-digit",
65
+ hour: "2-digit",
66
+ }).formatToParts(date);
67
+ const get = (type) => Number(parts.find((p) => p.type === type).value);
68
+ return { y: get("year"), m: get("month"), d: get("day"), h: get("hour") % 24 };
69
+ };
70
+ /** The bucket a moment falls in, as a string key. */
71
+ const keyOf = (date, unit, tz) => {
72
+ const { y, m, d, h } = localParts(date, tz);
73
+ if (unit === "hour")
74
+ return `${y}-${pad(m)}-${pad(d)} ${pad(h)}`;
75
+ if (unit === "month")
76
+ return `${y}-${pad(m)}`;
77
+ if (unit === "week") {
78
+ const weekday = new Date(Date.UTC(y, m - 1, d)).getUTCDay(); // 0 = Sunday
79
+ const monday = new Date(Date.UTC(y, m - 1, d - ((weekday + 6) % 7)));
80
+ return `${monday.getUTCFullYear()}-${pad(monday.getUTCMonth() + 1)}-${pad(monday.getUTCDate())}`;
81
+ }
82
+ return `${y}-${pad(m)}-${pad(d)}`;
83
+ };
84
+ exports.keyOf = keyOf;
85
+ /** Every bucket key between the range's start and end, in order. */
86
+ const bucketKeys = (range, unit, tz, now = new Date()) => {
87
+ const { start, end } = rangeBounds(range, now);
88
+ const step = unit === "hour" ? HOUR_MS : unit === "month" ? DAY_MS : 12 * HOUR_MS;
89
+ const keys = [];
90
+ const seen = new Set();
91
+ for (let t = start.getTime(); t <= end.getTime() + step; t += step) {
92
+ const at = new Date(Math.min(t, end.getTime()));
93
+ const key = (0, exports.keyOf)(at, unit, tz);
94
+ if (!seen.has(key)) {
95
+ seen.add(key);
96
+ keys.push(key);
97
+ if (keys.length >= MAX_BUCKETS)
98
+ break;
99
+ }
100
+ if (t >= end.getTime())
101
+ break;
102
+ }
103
+ return keys;
104
+ };
105
+ exports.bucketKeys = bucketKeys;
106
+ const hourLabel = (h) => `${h % 12 === 0 ? 12 : h % 12} ${h < 12 ? "AM" : "PM"}`;
107
+ /** A short axis label for a bucket key. */
108
+ const labelOf = (key, unit, multiYear = false) => {
109
+ if (unit === "hour")
110
+ return hourLabel(Number(key.slice(11, 13)));
111
+ const [y, m, d] = key.split("-");
112
+ if (unit === "month")
113
+ return multiYear ? `${MONTH_NAMES[Number(m) - 1]} '${y.slice(2)}` : MONTH_NAMES[Number(m) - 1];
114
+ return `${MONTH_NAMES[Number(m) - 1]} ${Number(d)}`;
115
+ };
116
+ exports.labelOf = labelOf;
117
+ /** A MongoDB expression that yields the same bucket key keyOf produces, for a date field. */
118
+ const groupKeyExpr = (unit, tz, field = "$createdAt") => {
119
+ if (unit === "hour")
120
+ return { $dateToString: { format: "%Y-%m-%d %H", date: field, timezone: tz } };
121
+ if (unit === "month")
122
+ return { $dateToString: { format: "%Y-%m", date: field, timezone: tz } };
123
+ if (unit === "week") {
124
+ return {
125
+ $dateToString: {
126
+ format: "%Y-%m-%d",
127
+ date: { $dateTrunc: { date: field, unit: "week", startOfWeek: "monday", timezone: tz } },
128
+ timezone: tz,
129
+ },
130
+ };
131
+ }
132
+ return { $dateToString: { format: "%Y-%m-%d", date: field, timezone: tz } };
133
+ };
134
+ exports.groupKeyExpr = groupKeyExpr;
135
+ /** Bucket keys, labels and the unit used, for a range. */
136
+ const buildAxis = (range, requested, tzInput, now = new Date()) => {
137
+ const tz = (0, exports.safeTimezone)(tzInput);
138
+ const unit = (0, exports.pickUnit)(range, requested, now);
139
+ const keys = (0, exports.bucketKeys)(range, unit, tz, now);
140
+ const multiYear = keys.length > 1 && keys[0].slice(0, 4) !== keys[keys.length - 1].slice(0, 4);
141
+ return { tz, unit, keys, labels: keys.map((k) => (0, exports.labelOf)(k, unit, multiYear)) };
142
+ };
143
+ exports.buildAxis = buildAxis;
@@ -0,0 +1,15 @@
1
+ export declare function createAvatarController({ companyIdOf, upload, remove }: {
2
+ companyIdOf: (req: any) => any;
3
+ upload: (file: any, folder: string) => Promise<{
4
+ publicUrl: string;
5
+ s3Key: string;
6
+ }>;
7
+ remove: (s3Key: string) => Promise<void>;
8
+ }): {
9
+ getAvatars: (req: any, res: any, next: any) => void;
10
+ createAvatar: (req: any, res: any, next: any) => void;
11
+ updateAvatar: (req: any, res: any, next: any) => void;
12
+ deleteAvatar: (req: any, res: any, next: any) => void;
13
+ setDefaultAvatar: (req: any, res: any, next: any) => void;
14
+ };
15
+ //# sourceMappingURL=avatars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatars.d.ts","sourceRoot":"","sources":["../../src/helpers/avatars.ts"],"names":[],"mappings":"AAcA,wBAAgB,sBAAsB,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;IACtE,WAAW,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;;;;;;EAiHA"}
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAvatarController = createAvatarController;
4
+ /**
5
+ * The chat avatars a company keeps (Company.avatars): list, add, rename/replace, delete, set default. Shared by the
6
+ * Ops backend (any company, from the URL) and the Client backend (the signed-in company):
7
+ * module.exports = createAvatarController({
8
+ * companyIdOf: (req) => req.user.owner, // or req.params.companyId
9
+ * upload: (file, folder) => <store it; resolves { publicUrl, s3Key }>,
10
+ * remove: (s3Key) => <delete it>,
11
+ * });
12
+ */
13
+ const mongoose_1 = require("mongoose");
14
+ const company_model_1 = require("../models/company.model");
15
+ const errors_1 = require("./errors");
16
+ const _http_1 = require("./_http");
17
+ function createAvatarController({ companyIdOf, upload, remove }) {
18
+ const CompanyModel = company_model_1.Company;
19
+ const getAvatars = (0, _http_1.catchAsync)(async (req, res, next) => {
20
+ var _a, _b, _c;
21
+ const company = await CompanyModel.findById(companyIdOf(req)).select("avatars").lean();
22
+ if (!company)
23
+ return next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
24
+ res.status(_http_1.HTTP.OK).json({
25
+ success: true,
26
+ results: (_b = (_a = company.avatars) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
27
+ data: { avatars: (_c = company.avatars) !== null && _c !== void 0 ? _c : [] },
28
+ });
29
+ });
30
+ // Body fields: name (text), avatar (file)
31
+ const createAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
32
+ var _a;
33
+ const { name } = req.body;
34
+ const file = req.file;
35
+ if (!name || !name.trim())
36
+ return next(new errors_1.AppError("Please provide a name for the avatar", _http_1.HTTP.BAD_REQUEST));
37
+ if (!file)
38
+ return next(new errors_1.AppError("Please upload an image file", _http_1.HTTP.BAD_REQUEST));
39
+ const company = await CompanyModel.findById(companyIdOf(req));
40
+ if (!company)
41
+ return next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
42
+ const { publicUrl, s3Key } = await upload(file, `avatars/${company._id}`);
43
+ // Never the default on creation.
44
+ company.avatars = (_a = company.avatars) !== null && _a !== void 0 ? _a : [];
45
+ company.avatars.push({ name: name.trim(), url: publicUrl, s3Key, isDefault: false });
46
+ await company.save();
47
+ res.status(_http_1.HTTP.CREATED).json({
48
+ success: true,
49
+ message: "Avatar uploaded successfully",
50
+ data: { avatar: company.avatars[company.avatars.length - 1] },
51
+ });
52
+ });
53
+ // Loads the company and one of its avatars, or answers the error.
54
+ async function findAvatar(req, next) {
55
+ var _a;
56
+ const { avatarId } = req.params;
57
+ if (!mongoose_1.Types.ObjectId.isValid(avatarId)) {
58
+ next(new errors_1.AppError("Invalid avatar ID", _http_1.HTTP.BAD_REQUEST));
59
+ return null;
60
+ }
61
+ const company = await CompanyModel.findById(companyIdOf(req));
62
+ if (!company) {
63
+ next(new errors_1.AppError("Company not found", _http_1.HTTP.NOT_FOUND));
64
+ return null;
65
+ }
66
+ const avatar = ((_a = company.avatars) !== null && _a !== void 0 ? _a : []).id(avatarId);
67
+ if (!avatar) {
68
+ next(new errors_1.AppError("Avatar not found", _http_1.HTTP.NOT_FOUND));
69
+ return null;
70
+ }
71
+ return { company, avatar, avatarId };
72
+ }
73
+ // Body fields (all optional): name (text), avatar (file)
74
+ const updateAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
75
+ const found = await findAvatar(req, next);
76
+ if (!found)
77
+ return;
78
+ const { company, avatar } = found;
79
+ const { name } = req.body;
80
+ const file = req.file;
81
+ if (!name && !file)
82
+ return next(new errors_1.AppError("Please provide a name or a new image to update", _http_1.HTTP.BAD_REQUEST));
83
+ if (name && name.trim())
84
+ avatar.name = name.trim();
85
+ if (file) {
86
+ const oldS3Key = avatar.s3Key;
87
+ const { publicUrl, s3Key } = await upload(file, `avatars/${company._id}`);
88
+ avatar.url = publicUrl;
89
+ avatar.s3Key = s3Key;
90
+ // The old file goes only after the new one is stored.
91
+ await remove(oldS3Key);
92
+ }
93
+ await company.save();
94
+ res.status(_http_1.HTTP.OK).json({ success: true, message: "Avatar updated successfully", data: { avatar } });
95
+ });
96
+ const deleteAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
97
+ const found = await findAvatar(req, next);
98
+ if (!found)
99
+ return;
100
+ const { company, avatar } = found;
101
+ const s3KeyToDelete = avatar.s3Key;
102
+ avatar.deleteOne();
103
+ await company.save();
104
+ // A file that fails to delete is logged, never an error for the user.
105
+ remove(s3KeyToDelete).catch((err) => console.warn(`Failed to delete S3 object '${s3KeyToDelete}': ${err.message}`));
106
+ res.status(_http_1.HTTP.OK).json({ success: true, message: "Avatar deleted successfully" });
107
+ });
108
+ // Only one avatar can be the default.
109
+ const setDefaultAvatar = (0, _http_1.catchAsync)(async (req, res, next) => {
110
+ var _a;
111
+ const found = await findAvatar(req, next);
112
+ if (!found)
113
+ return;
114
+ const { company, avatar, avatarId } = found;
115
+ ((_a = company.avatars) !== null && _a !== void 0 ? _a : []).forEach((a) => {
116
+ a.isDefault = a._id.toString() === avatarId;
117
+ });
118
+ await company.save();
119
+ res.status(_http_1.HTTP.OK).json({ success: true, message: "Default avatar set successfully", data: { avatar } });
120
+ });
121
+ return { getAvatars, createAvatar, updateAvatar, deleteAvatar, setDefaultAvatar };
122
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a chat carries besides text: the files the visitor shared, and the delivery status of every SMS an agent
3
+ * sent. Used by the Employee, Ops and Client backends, so all three dashboards show the same thing.
4
+ */
5
+ export declare const fileNameFromUrl: (url: string) => string;
6
+ export interface VisitorDocument {
7
+ id: string;
8
+ url: string;
9
+ name: string;
10
+ kind: string;
11
+ sharedAt: Date;
12
+ }
13
+ export interface SmsStatusRow {
14
+ id: number;
15
+ sid: string | null;
16
+ status: string;
17
+ from: string | null;
18
+ to: string | null;
19
+ sentAt: Date;
20
+ statusUpdatedAt: Date;
21
+ errorCode: string | number | null;
22
+ errorMessage: string | null;
23
+ preview: string;
24
+ }
25
+ /** Files the visitor shared in the chat, oldest first. */
26
+ export declare function getVisitorDocuments(chatId: unknown): Promise<VisitorDocument[]>;
27
+ /** Every SMS an agent sent in the chat, oldest first, each with its own running number. */
28
+ export declare function getSmsStatus(chatId: unknown): Promise<SmsStatusRow[]>;
29
+ export declare function getChatAttachments(chatId: unknown): Promise<{
30
+ documents: VisitorDocument[];
31
+ sms: SmsStatusRow[];
32
+ }>;
33
+ //# sourceMappingURL=chatAttachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatAttachments.d.ts","sourceRoot":"","sources":["../../src/helpers/chatAttachments.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAGH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,MAQ7C,CAAC;AAWF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,CAAC;IACb,eAAe,EAAE,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0DAA0D;AAC1D,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAkBrF;AAED,2FAA2F;AAC3F,wBAAsB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAoB3E;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,eAAe,EAAE,CAAC;IAAC,GAAG,EAAE,YAAY,EAAE,CAAA;CAAE,CAAC,CAGxH"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileNameFromUrl = void 0;
4
+ exports.getVisitorDocuments = getVisitorDocuments;
5
+ exports.getSmsStatus = getSmsStatus;
6
+ exports.getChatAttachments = getChatAttachments;
7
+ const mongoose_1 = require("mongoose");
8
+ const message_model_1 = require("../models/message.model");
9
+ /**
10
+ * What a chat carries besides text: the files the visitor shared, and the delivery status of every SMS an agent
11
+ * sent. Used by the Employee, Ops and Client backends, so all three dashboards show the same thing.
12
+ */
13
+ // Files are stored as <folder>/<uuid>-<original name>; the uuid has five dash-separated parts.
14
+ const fileNameFromUrl = (url) => {
15
+ try {
16
+ const last = decodeURIComponent(new URL(url).pathname.split("/").pop() || "");
17
+ const parts = last.split("-");
18
+ return (parts.length > 5 ? parts.slice(5).join("-") : last) || "File";
19
+ }
20
+ catch {
21
+ return "File";
22
+ }
23
+ };
24
+ exports.fileNameFromUrl = fileNameFromUrl;
25
+ const kindOf = (mediaType, url) => {
26
+ if (mediaType)
27
+ return mediaType;
28
+ if (/\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(url))
29
+ return "image";
30
+ if (/\.pdf(\?|$)/i.test(url))
31
+ return "pdf";
32
+ if (/\.(mp4|webm|mov)(\?|$)/i.test(url))
33
+ return "video";
34
+ if (/\.(mp3|wav|ogg)(\?|$)/i.test(url))
35
+ return "audio";
36
+ return "file";
37
+ };
38
+ /** Files the visitor shared in the chat, oldest first. */
39
+ async function getVisitorDocuments(chatId) {
40
+ const rows = await message_model_1.Message.find({
41
+ chatId,
42
+ isDeleted: { $ne: true },
43
+ senderType: "VISITOR",
44
+ mediaUrl: { $exists: true, $nin: [null, ""] },
45
+ })
46
+ .sort({ createdAt: 1 })
47
+ .select("mediaUrl mediaType createdAt")
48
+ .lean();
49
+ return rows.map((m) => ({
50
+ id: String(m._id),
51
+ url: m.mediaUrl,
52
+ name: (0, exports.fileNameFromUrl)(m.mediaUrl),
53
+ kind: kindOf(m.mediaType, m.mediaUrl),
54
+ sharedAt: m.createdAt,
55
+ }));
56
+ }
57
+ /** Every SMS an agent sent in the chat, oldest first, each with its own running number. */
58
+ async function getSmsStatus(chatId) {
59
+ // `sms` is written by chat-service with the native driver, so it is read the same way.
60
+ const rows = await message_model_1.Message.collection
61
+ .find({ chatId: new mongoose_1.Types.ObjectId(String(chatId)), isDeleted: { $ne: true }, "sms.seq": { $exists: true } })
62
+ .sort({ createdAt: 1 })
63
+ .project({ sms: 1, content: 1, createdAt: 1 })
64
+ .toArray();
65
+ return rows.map((m) => ({
66
+ id: m.sms.seq,
67
+ sid: m.sms.sid || null,
68
+ status: m.sms.status || "queued",
69
+ from: m.sms.from || null,
70
+ to: m.sms.to || null,
71
+ sentAt: m.createdAt,
72
+ statusUpdatedAt: m.sms.statusUpdatedAt || m.createdAt,
73
+ errorCode: m.sms.errorCode || null,
74
+ errorMessage: m.sms.errorMessage || null,
75
+ preview: String(m.content || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120),
76
+ }));
77
+ }
78
+ async function getChatAttachments(chatId) {
79
+ const [documents, sms] = await Promise.all([getVisitorDocuments(chatId), getSmsStatus(chatId)]);
80
+ return { documents, sms };
81
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Turns encrypted lead contact details (email/phone tokens) back into real values. Used by the Ops and Client
3
+ * backends; each one only says how it reaches chat-service's decryption:
4
+ * const { decryptLeadContactInfo, decryptTokensInTexts } = createContactDecryptor({
5
+ * requestDecrypt: async (field, value) => <the JSON reply of the decrypt route for { [field]: value }>,
6
+ * });
7
+ * Best-effort: a value that can't be decrypted shows as "Encrypted" (never ciphertext) and never breaks the caller.
8
+ */
9
+ export declare const unwrapPlainEmail: (value: any) => any;
10
+ export declare const looksEncrypted: (value: any) => boolean;
11
+ export type ContactField = "email" | "phone";
12
+ export declare function createContactDecryptor({ requestDecrypt }: {
13
+ requestDecrypt: (field: ContactField, value: string) => Promise<any>;
14
+ }): {
15
+ decryptLeadContactInfo: (raw: {
16
+ email?: string;
17
+ phone?: string;
18
+ } | null | undefined) => Promise<{
19
+ email?: string;
20
+ phone?: string;
21
+ }>;
22
+ decryptTokensInTexts: (texts: string[]) => Promise<string[]>;
23
+ looksEncrypted: (value: any) => boolean;
24
+ unwrapPlainEmail: (value: any) => any;
25
+ };
26
+ //# sourceMappingURL=contactDecrypt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contactDecrypt.d.ts","sourceRoot":"","sources":["../../src/helpers/contactDecrypt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,eAAO,MAAM,gBAAgB,GAAI,OAAO,GAAG,KAAG,GAG7C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,OAAO,GAAG,KAAG,OACmC,CAAC;AAehF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;AAE7C,wBAAgB,sBAAsB,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;CAAE;kCActF;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS;gBACtE,MAAM;gBAAU,MAAM;;kCAmBL,MAAM,EAAE,KAAG,OAAO,CAAC,MAAM,EAAE,CAAC;4BApDnC,GAAG,KAAG,OAAO;8BALX,GAAG,KAAG,GAAG;EAmFhD"}