orbitx-core-models 11.175.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.
- package/dist/helpers/_http.d.ts +10 -0
- package/dist/helpers/_http.d.ts.map +1 -0
- package/dist/helpers/_http.js +16 -0
- package/dist/helpers/avatars.d.ts +15 -0
- package/dist/helpers/avatars.d.ts.map +1 -0
- package/dist/helpers/avatars.js +122 -0
- package/dist/helpers/contactDecrypt.d.ts +26 -0
- package/dist/helpers/contactDecrypt.d.ts.map +1 -0
- package/dist/helpers/contactDecrypt.js +95 -0
- package/dist/helpers/emailTemplates.d.ts +19 -0
- package/dist/helpers/emailTemplates.d.ts.map +1 -0
- package/dist/helpers/emailTemplates.js +276 -0
- package/dist/helpers/invoiceDocument.d.ts +63 -0
- package/dist/helpers/invoiceDocument.d.ts.map +1 -0
- package/dist/helpers/invoiceDocument.js +148 -0
- package/dist/helpers/myUpdates.d.ts +16 -0
- package/dist/helpers/myUpdates.d.ts.map +1 -0
- package/dist/helpers/myUpdates.js +94 -0
- package/dist/helpers/ownActivity.d.ts +8 -0
- package/dist/helpers/ownActivity.d.ts.map +1 -0
- package/dist/helpers/ownActivity.js +114 -0
- package/dist/helpers/ownTickets.d.ts +21 -0
- package/dist/helpers/ownTickets.d.ts.map +1 -0
- package/dist/helpers/ownTickets.js +169 -0
- package/dist/helpers/s3Storage.d.ts +38 -0
- package/dist/helpers/s3Storage.d.ts.map +1 -0
- package/dist/helpers/s3Storage.js +73 -0
- package/dist/helpers/sessions.d.ts +22 -0
- package/dist/helpers/sessions.d.ts.map +1 -0
- package/dist/helpers/sessions.js +61 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Who the invoice is from (owner, 2026-09-29). */
|
|
2
|
+
export declare const INVOICE_ISSUER: {
|
|
3
|
+
name: string;
|
|
4
|
+
addressLines: string[];
|
|
5
|
+
email: string;
|
|
6
|
+
};
|
|
7
|
+
/** The statuses a client can see; drafts and invoices waiting for approval stay inside Ops. */
|
|
8
|
+
export declare const CLIENT_VISIBLE_INVOICE_STATUSES: string[];
|
|
9
|
+
/** Approved invoices are charged together on the 5th of the month. */
|
|
10
|
+
export declare function nextInvoiceChargeDate(from?: Date): Date;
|
|
11
|
+
export declare function createInvoiceDocuments({ stripe }: {
|
|
12
|
+
stripe: any;
|
|
13
|
+
}): {
|
|
14
|
+
ISSUER: {
|
|
15
|
+
name: string;
|
|
16
|
+
addressLines: string[];
|
|
17
|
+
email: string;
|
|
18
|
+
};
|
|
19
|
+
nextInvoiceNumber: (at?: Date) => Promise<string>;
|
|
20
|
+
ensureInvoiceNumber: (invoice: any) => Promise<string>;
|
|
21
|
+
nextChargeDate: typeof nextInvoiceChargeDate;
|
|
22
|
+
buildInvoiceDocument: (invoiceId: any, { companyId, clientView }?: {
|
|
23
|
+
companyId?: any;
|
|
24
|
+
clientView?: boolean;
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
invoice: {
|
|
27
|
+
_id: any;
|
|
28
|
+
number: string;
|
|
29
|
+
status: any;
|
|
30
|
+
issueDate: any;
|
|
31
|
+
periodStart: any;
|
|
32
|
+
periodEnd: any;
|
|
33
|
+
subtotal: any;
|
|
34
|
+
adjustments: any;
|
|
35
|
+
totalDue: any;
|
|
36
|
+
approvedAt: any;
|
|
37
|
+
paidAt: any;
|
|
38
|
+
chargeDate: Date | null;
|
|
39
|
+
stripeInvoiceUrl: any;
|
|
40
|
+
};
|
|
41
|
+
from: {
|
|
42
|
+
name: string;
|
|
43
|
+
addressLines: string[];
|
|
44
|
+
email: string;
|
|
45
|
+
};
|
|
46
|
+
billTo: {
|
|
47
|
+
name: any;
|
|
48
|
+
companyId: any;
|
|
49
|
+
address: any;
|
|
50
|
+
city: any;
|
|
51
|
+
state: any;
|
|
52
|
+
zip: any;
|
|
53
|
+
country: any;
|
|
54
|
+
email: any;
|
|
55
|
+
};
|
|
56
|
+
items: any;
|
|
57
|
+
card: {
|
|
58
|
+
brand: any;
|
|
59
|
+
last4: any;
|
|
60
|
+
} | null;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=invoiceDocument.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoiceDocument.d.ts","sourceRoot":"","sources":["../../src/helpers/invoiceDocument.ts"],"names":[],"mappings":"AAaA,mDAAmD;AACnD,eAAO,MAAM,cAAc;;;;CAI1B,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,+BAA+B,UAA8D,CAAC;AAE3G,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,IAAI,OAAa,GAAG,IAAI,CAI7D;AAED,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,GAAG,CAAA;CAAE;;;;;;sCAOb,OAAO,CAAC,MAAM,CAAC;mCAOtB,GAAG,KAAG,OAAO,CAAC,MAAM,CAAC;;sCAmClB,GAAG,8BAAqC;QAAE,SAAS,CAAC,EAAE,GAAG,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoEjI"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLIENT_VISIBLE_INVOICE_STATUSES = exports.INVOICE_ISSUER = void 0;
|
|
4
|
+
exports.nextInvoiceChargeDate = nextInvoiceChargeDate;
|
|
5
|
+
exports.createInvoiceDocuments = createInvoiceDocuments;
|
|
6
|
+
/**
|
|
7
|
+
* The invoice as a document: what the invoice page in Ops and the Client dashboard shows (and what prints as the
|
|
8
|
+
* PDF), plus invoice numbers. Shared by the Ops and Client backends; each passes its Stripe client:
|
|
9
|
+
* const invoices = createInvoiceDocuments({ stripe });
|
|
10
|
+
* invoices.buildInvoiceDocument(invoiceId) // Ops: any invoice
|
|
11
|
+
* invoices.buildInvoiceDocument(invoiceId, { companyId, clientView: true }) // Client: its own, past approval
|
|
12
|
+
*/
|
|
13
|
+
const company_model_1 = require("../models/company.model");
|
|
14
|
+
const counter_model_1 = require("../models/counter.model");
|
|
15
|
+
const invoice_model_1 = require("../models/invoice.model");
|
|
16
|
+
const leadLedger_model_1 = require("../models/leadLedger.model");
|
|
17
|
+
const errors_1 = require("./errors");
|
|
18
|
+
/** Who the invoice is from (owner, 2026-09-29). */
|
|
19
|
+
exports.INVOICE_ISSUER = {
|
|
20
|
+
name: "ORBITXPRO LLC",
|
|
21
|
+
addressLines: ["4926 Winding View Ln", "Humble, TX 77346"],
|
|
22
|
+
email: "billing@orbitxpro.com",
|
|
23
|
+
};
|
|
24
|
+
/** The statuses a client can see; drafts and invoices waiting for approval stay inside Ops. */
|
|
25
|
+
exports.CLIENT_VISIBLE_INVOICE_STATUSES = ["approved", "pending_payment", "paid", "void", "disputed"];
|
|
26
|
+
/** Approved invoices are charged together on the 5th of the month. */
|
|
27
|
+
function nextInvoiceChargeDate(from = new Date()) {
|
|
28
|
+
const date = new Date(from.getFullYear(), from.getMonth(), 5, 12, 0, 0);
|
|
29
|
+
if (from.getDate() > 5)
|
|
30
|
+
date.setMonth(date.getMonth() + 1);
|
|
31
|
+
return date;
|
|
32
|
+
}
|
|
33
|
+
function createInvoiceDocuments({ stripe }) {
|
|
34
|
+
const InvoiceModel = invoice_model_1.Invoice;
|
|
35
|
+
const CompanyModel = company_model_1.Company;
|
|
36
|
+
const CounterModel = counter_model_1.Counter;
|
|
37
|
+
const LeadLedgerModel = leadLedger_model_1.LeadLedger;
|
|
38
|
+
/** "INV-2026-0001": sequential across OrbitXPro, starting again each year. */
|
|
39
|
+
async function nextInvoiceNumber(at = new Date()) {
|
|
40
|
+
const year = at.getFullYear();
|
|
41
|
+
const counter = await CounterModel.findByIdAndUpdate(`invoiceNumber_${year}`, { $inc: { seq: 1 } }, { new: true, upsert: true });
|
|
42
|
+
return `INV-${year}-${String(counter.seq).padStart(4, "0")}`;
|
|
43
|
+
}
|
|
44
|
+
/** Invoices made before numbering get theirs the first time they're shown. */
|
|
45
|
+
async function ensureInvoiceNumber(invoice) {
|
|
46
|
+
if (invoice.invoiceNumber)
|
|
47
|
+
return invoice.invoiceNumber;
|
|
48
|
+
const number = await nextInvoiceNumber(new Date(invoice.createdAt || Date.now()));
|
|
49
|
+
const updated = await InvoiceModel.findOneAndUpdate({ _id: invoice._id, invoiceNumber: { $exists: false } }, { $set: { invoiceNumber: number } }, { new: true }).lean();
|
|
50
|
+
if (updated === null || updated === void 0 ? void 0 : updated.invoiceNumber)
|
|
51
|
+
return updated.invoiceNumber;
|
|
52
|
+
const current = await InvoiceModel.findById(invoice._id).select("invoiceNumber").lean();
|
|
53
|
+
return (current === null || current === void 0 ? void 0 : current.invoiceNumber) || number;
|
|
54
|
+
}
|
|
55
|
+
async function cardFor(invoice, company) {
|
|
56
|
+
var _a, _b, _c, _d;
|
|
57
|
+
try {
|
|
58
|
+
if (invoice.stripeInvoiceId) {
|
|
59
|
+
const si = await stripe.invoices.retrieve(invoice.stripeInvoiceId, { expand: ["payment_intent.payment_method"] });
|
|
60
|
+
const card = (_b = (_a = si === null || si === void 0 ? void 0 : si.payment_intent) === null || _a === void 0 ? void 0 : _a.payment_method) === null || _b === void 0 ? void 0 : _b.card;
|
|
61
|
+
if (card)
|
|
62
|
+
return { brand: card.brand, last4: card.last4 };
|
|
63
|
+
}
|
|
64
|
+
if (company === null || company === void 0 ? void 0 : company.stripeCustomerId) {
|
|
65
|
+
const customer = await stripe.customers.retrieve(company.stripeCustomerId, { expand: ["invoice_settings.default_payment_method"] });
|
|
66
|
+
const card = (_d = (_c = customer === null || customer === void 0 ? void 0 : customer.invoice_settings) === null || _c === void 0 ? void 0 : _c.default_payment_method) === null || _d === void 0 ? void 0 : _d.card;
|
|
67
|
+
if (card)
|
|
68
|
+
return { brand: card.brand, last4: card.last4 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`[invoice] card for ${invoice._id} not read:`, err.message);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Everything the invoice page shows. `companyId` limits it to that company's own invoices; `clientView` also
|
|
78
|
+
* hides drafts/unapproved invoices and the Stripe link (Client dashboard).
|
|
79
|
+
*/
|
|
80
|
+
async function buildInvoiceDocument(invoiceId, { companyId, clientView = false } = {}) {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
const invoice = clientView
|
|
83
|
+
? await InvoiceModel.findOne({ _id: invoiceId, companyId, status: { $in: exports.CLIENT_VISIBLE_INVOICE_STATUSES } }).lean().catch(() => null)
|
|
84
|
+
: await InvoiceModel.findById(invoiceId).lean().catch(() => null);
|
|
85
|
+
if (!invoice || (companyId && String(invoice.companyId) !== String(companyId)))
|
|
86
|
+
throw new errors_1.AppError("Invoice not found", 404);
|
|
87
|
+
const [company, number, entries] = await Promise.all([
|
|
88
|
+
CompanyModel.findById(invoice.companyId).select("contactInfo basicInfo.companyId stripeCustomerId").lean(),
|
|
89
|
+
ensureInvoiceNumber(invoice),
|
|
90
|
+
LeadLedgerModel.find({ invoiceId: invoice._id, status: { $ne: "discarded" } })
|
|
91
|
+
.sort({ createdAt: 1 })
|
|
92
|
+
.populate({ path: "leadId", select: "chatId data.name", populate: { path: "chatId", select: "chatId chatNumber" } })
|
|
93
|
+
.lean(),
|
|
94
|
+
]);
|
|
95
|
+
const card = await cardFor(invoice, company);
|
|
96
|
+
const contact = (company === null || company === void 0 ? void 0 : company.contactInfo) || {};
|
|
97
|
+
const items = entries.map((e) => {
|
|
98
|
+
var _a, _b, _c;
|
|
99
|
+
const quantity = e.quantity || 1;
|
|
100
|
+
const sign = e.type === "deduct" ? -1 : 1;
|
|
101
|
+
let description;
|
|
102
|
+
if (e.type === "lead") {
|
|
103
|
+
const chat = (_a = e.leadId) === null || _a === void 0 ? void 0 : _a.chatId;
|
|
104
|
+
const ref = (chat === null || chat === void 0 ? void 0 : chat.chatId) || ((chat === null || chat === void 0 ? void 0 : chat.chatNumber) != null ? String(chat.chatNumber) : "");
|
|
105
|
+
const name = (_c = (_b = e.leadId) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.name;
|
|
106
|
+
description = `Lead${ref ? ` ${ref}` : ""}${name ? ` · ${name}` : ""}`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
description = e.note || (e.type === "add" ? "Additional charge" : "Credit");
|
|
110
|
+
}
|
|
111
|
+
return { date: e.createdAt, type: e.type, description, quantity, unitPrice: e.unitPrice, amount: sign * e.unitPrice * quantity };
|
|
112
|
+
});
|
|
113
|
+
const chargeStatuses = clientView ? ["approved"] : ["draft", "pending_approval", "approved"];
|
|
114
|
+
return {
|
|
115
|
+
invoice: {
|
|
116
|
+
_id: invoice._id,
|
|
117
|
+
number,
|
|
118
|
+
status: invoice.status,
|
|
119
|
+
issueDate: invoice.createdAt,
|
|
120
|
+
periodStart: invoice.billingPeriodStart,
|
|
121
|
+
periodEnd: invoice.billingPeriodEnd,
|
|
122
|
+
subtotal: invoice.subtotal,
|
|
123
|
+
adjustments: invoice.adjustments,
|
|
124
|
+
totalDue: invoice.totalDue,
|
|
125
|
+
approvedAt: invoice.approvedAt || null,
|
|
126
|
+
paidAt: invoice.paidAt || null,
|
|
127
|
+
chargeDate: chargeStatuses.includes(invoice.status)
|
|
128
|
+
? nextInvoiceChargeDate(invoice.approvedAt ? new Date(invoice.approvedAt) : new Date())
|
|
129
|
+
: null,
|
|
130
|
+
stripeInvoiceUrl: clientView ? null : invoice.stripeInvoiceUrl || null,
|
|
131
|
+
},
|
|
132
|
+
from: exports.INVOICE_ISSUER,
|
|
133
|
+
billTo: {
|
|
134
|
+
name: contact.officialBusinessName || ((_a = company === null || company === void 0 ? void 0 : company.basicInfo) === null || _a === void 0 ? void 0 : _a.companyId) || "",
|
|
135
|
+
companyId: ((_b = company === null || company === void 0 ? void 0 : company.basicInfo) === null || _b === void 0 ? void 0 : _b.companyId) || "",
|
|
136
|
+
address: contact.address || "",
|
|
137
|
+
city: contact.city || "",
|
|
138
|
+
state: contact.state || "",
|
|
139
|
+
zip: contact.zipcode || "",
|
|
140
|
+
country: contact.country || "",
|
|
141
|
+
email: contact.email || "",
|
|
142
|
+
},
|
|
143
|
+
items,
|
|
144
|
+
card,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { ISSUER: exports.INVOICE_ISSUER, nextInvoiceNumber, ensureInvoiceNumber, nextChargeDate: nextInvoiceChargeDate, buildInvoiceDocument };
|
|
148
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function createMyUpdatesController({ dashboard, audience, ownerKey, sse }: {
|
|
2
|
+
dashboard: string;
|
|
3
|
+
audience: string;
|
|
4
|
+
ownerKey: string;
|
|
5
|
+
sse: {
|
|
6
|
+
addClient: (res: any) => void;
|
|
7
|
+
removeClient: (res: any) => void;
|
|
8
|
+
};
|
|
9
|
+
}): {
|
|
10
|
+
getMyUpdates: (req: any, res: any, next: any) => void;
|
|
11
|
+
markRead: (req: any, res: any, next: any) => void;
|
|
12
|
+
markAllRead: (req: any, res: any, next: any) => void;
|
|
13
|
+
markReadByLink: (req: any, res: any, next: any) => void;
|
|
14
|
+
streamUpdates: (req: any, res: any) => void;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=myUpdates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"myUpdates.d.ts","sourceRoot":"","sources":["../../src/helpers/myUpdates.ts"],"names":[],"mappings":"AAYA,wBAAgB,yBAAyB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE;QAAE,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;QAAC,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;KAAE,CAAC;CAC1E;;;;;yBA2D6B,GAAG,OAAO,GAAG;EAyB1C"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createMyUpdatesController = createMyUpdatesController;
|
|
7
|
+
/**
|
|
8
|
+
* A portal user's Updates feed (the bell/Updates page): list with unread counts, mark read, mark all read, mark
|
|
9
|
+
* read by link, and the live "something changed" stream. Shared by the Client and Partner backends:
|
|
10
|
+
* module.exports = createMyUpdatesController({ dashboard: "client", audience: "company", ownerKey: "companyId", sse });
|
|
11
|
+
* module.exports = createMyUpdatesController({ dashboard: "partner", audience: "partner", ownerKey: "partnerId", sse });
|
|
12
|
+
* `sse` is the backend's own stream service ({ addClient(res), removeClient(res) }).
|
|
13
|
+
*/
|
|
14
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
15
|
+
const update_model_1 = require("../models/update.model");
|
|
16
|
+
const errors_1 = require("./errors");
|
|
17
|
+
const _http_1 = require("./_http");
|
|
18
|
+
function createMyUpdatesController({ dashboard, audience, ownerKey, sse }) {
|
|
19
|
+
const UpdateModel = update_model_1.Update;
|
|
20
|
+
// Mine: a broadcast to every user of this dashboard, something aimed at my whole account (company or partner),
|
|
21
|
+
// or aimed at me.
|
|
22
|
+
const myUpdatesQuery = (userId, ownerId) => ({
|
|
23
|
+
dashboard,
|
|
24
|
+
$or: [{ audience: "broadcast" }, { audience, [ownerKey]: ownerId }, { audience: "user", userId }],
|
|
25
|
+
});
|
|
26
|
+
const getMyUpdates = (0, _http_1.catchAsync)(async (req, res) => {
|
|
27
|
+
const query = myUpdatesQuery(req.user._id, req.user.owner);
|
|
28
|
+
const unreadQuery = { ...query, readBy: { $ne: req.user._id } };
|
|
29
|
+
const [updates, unreadCount, unreadGroups] = await Promise.all([
|
|
30
|
+
UpdateModel.find(query).sort({ createdAt: -1 }).limit(100).lean(),
|
|
31
|
+
UpdateModel.countDocuments(unreadQuery),
|
|
32
|
+
// Unread count per type, so the sidebar can badge Tickets and Requests separately from Updates.
|
|
33
|
+
UpdateModel.aggregate([{ $match: unreadQuery }, { $group: { _id: "$type", count: { $sum: 1 } } }]),
|
|
34
|
+
]);
|
|
35
|
+
const unreadByType = Object.fromEntries(unreadGroups.map((g) => [g._id, g.count]));
|
|
36
|
+
res.status(_http_1.HTTP.OK).json({
|
|
37
|
+
success: true,
|
|
38
|
+
results: updates.length,
|
|
39
|
+
data: {
|
|
40
|
+
updates: updates.map((u) => ({
|
|
41
|
+
...u,
|
|
42
|
+
isRead: (u.readBy || []).some((id) => String(id) === String(req.user._id)),
|
|
43
|
+
})),
|
|
44
|
+
unreadCount,
|
|
45
|
+
unreadByType,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const markRead = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
50
|
+
if (!mongoose_1.default.Types.ObjectId.isValid(req.params.id))
|
|
51
|
+
return next(new errors_1.AppError("Invalid update id", _http_1.HTTP.BAD_REQUEST));
|
|
52
|
+
const update = await UpdateModel.findByIdAndUpdate(req.params.id, { $addToSet: { readBy: req.user._id } }, { new: true });
|
|
53
|
+
if (!update)
|
|
54
|
+
return next(new errors_1.AppError("Update not found", _http_1.HTTP.NOT_FOUND));
|
|
55
|
+
res.status(_http_1.HTTP.OK).json({ success: true, message: "Marked read" });
|
|
56
|
+
});
|
|
57
|
+
const markAllRead = (0, _http_1.catchAsync)(async (req, res) => {
|
|
58
|
+
await UpdateModel.updateMany(myUpdatesQuery(req.user._id, req.user.owner), { $addToSet: { readBy: req.user._id } });
|
|
59
|
+
res.status(_http_1.HTTP.OK).json({ success: true, message: "All updates marked read" });
|
|
60
|
+
});
|
|
61
|
+
// Opening a ticket/request marks its updates read, so its sidebar badge clears. Body: { link: "/tickets/view/<id>" }.
|
|
62
|
+
const markReadByLink = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
63
|
+
var _a;
|
|
64
|
+
const link = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.link) === "string" ? req.body.link.trim() : "";
|
|
65
|
+
if (!link || link.length > 300)
|
|
66
|
+
return next(new errors_1.AppError("A link is required", _http_1.HTTP.BAD_REQUEST));
|
|
67
|
+
const result = await UpdateModel.updateMany({ ...myUpdatesQuery(req.user._id, req.user.owner), link, readBy: { $ne: req.user._id } }, { $addToSet: { readBy: req.user._id } });
|
|
68
|
+
res.status(_http_1.HTTP.OK).json({ success: true, data: { marked: result.modifiedCount || 0 } });
|
|
69
|
+
});
|
|
70
|
+
// A bare "something changed" ping; the page reacts by reloading its own feed.
|
|
71
|
+
const streamUpdates = (req, res) => {
|
|
72
|
+
res.writeHead(_http_1.HTTP.OK, {
|
|
73
|
+
"Content-Type": "text/event-stream",
|
|
74
|
+
"Cache-Control": "no-cache",
|
|
75
|
+
Connection: "keep-alive",
|
|
76
|
+
"X-Accel-Buffering": "no",
|
|
77
|
+
});
|
|
78
|
+
res.write("\n");
|
|
79
|
+
sse.addClient(res);
|
|
80
|
+
const heartbeat = setInterval(() => {
|
|
81
|
+
try {
|
|
82
|
+
res.write(": ping\n\n");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
clearInterval(heartbeat);
|
|
86
|
+
}
|
|
87
|
+
}, 25000);
|
|
88
|
+
req.on("close", () => {
|
|
89
|
+
clearInterval(heartbeat);
|
|
90
|
+
sse.removeClient(res);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
return { getMyUpdates, markRead, markAllRead, markReadByLink, streamUpdates };
|
|
94
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function createOwnActivityController({ kind }: {
|
|
2
|
+
kind: "client" | "partner";
|
|
3
|
+
}): {
|
|
4
|
+
getAccess: (req: any, res: any, next: any) => void;
|
|
5
|
+
listActivity: (req: any, res: any, next: any) => void;
|
|
6
|
+
recordExport: (req: any, res: any, next: any) => void;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=ownActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownActivity.d.ts","sourceRoot":"","sources":["../../src/helpers/ownActivity.ts"],"names":[],"mappings":"AA2BA,wBAAgB,2BAA2B,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE;;;;EAmFnF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createOwnActivityController = createOwnActivityController;
|
|
7
|
+
/**
|
|
8
|
+
* A portal's own Activity log: everything done on its account, by its own users and by OrbitXPro staff (HIPAA: an
|
|
9
|
+
* account must be able to see everyone who touched its data). Shown only after Ops turns on the account's access
|
|
10
|
+
* (activityLogAccess). Shared by the Client and Partner backends:
|
|
11
|
+
* createOwnActivityController({ kind: "client" }) // Company accounts, logs filed under `company`
|
|
12
|
+
* createOwnActivityController({ kind: "partner" }) // Partner accounts, logs filed under `partner`
|
|
13
|
+
*/
|
|
14
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
15
|
+
const company_model_1 = require("../models/company.model");
|
|
16
|
+
const partner_model_1 = require("../models/partner.model");
|
|
17
|
+
const activityLog_model_1 = require("../models/activityLog.model");
|
|
18
|
+
const recorder_1 = require("../activity/recorder");
|
|
19
|
+
const errors_1 = require("./errors");
|
|
20
|
+
const _http_1 = require("./_http");
|
|
21
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
const list = (v) => String(v || "")
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((s) => s.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
const KINDS = {
|
|
27
|
+
client: { Account: company_model_1.Company, field: "company", searchFields: ["actor.name", "actor.email", "details", "action", "record.label"] },
|
|
28
|
+
partner: { Account: partner_model_1.Partner, field: "partner", searchFields: ["actor.name", "actor.email", "details", "action"] },
|
|
29
|
+
};
|
|
30
|
+
function createOwnActivityController({ kind }) {
|
|
31
|
+
const { field, searchFields } = KINDS[kind];
|
|
32
|
+
const Account = KINDS[kind].Account;
|
|
33
|
+
const ActivityLogModel = activityLog_model_1.ActivityLog;
|
|
34
|
+
const accountIdOf = (req) => { var _a, _b, _c; return ((_b = (_a = req.user) === null || _a === void 0 ? void 0 : _a.owner) === null || _b === void 0 ? void 0 : _b._id) || ((_c = req.user) === null || _c === void 0 ? void 0 : _c.owner); };
|
|
35
|
+
const getAccess = (0, _http_1.catchAsync)(async (req, res) => {
|
|
36
|
+
const account = await Account.findById(accountIdOf(req)).select("activityLogAccess").lean();
|
|
37
|
+
res.status(_http_1.HTTP.OK).json({ success: true, data: { enabled: !!(account === null || account === void 0 ? void 0 : account.activityLogAccess) } });
|
|
38
|
+
});
|
|
39
|
+
const listActivity = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
40
|
+
const account = await Account.findById(accountIdOf(req)).select("activityLogAccess").lean();
|
|
41
|
+
if (!(account === null || account === void 0 ? void 0 : account.activityLogAccess)) {
|
|
42
|
+
return next(new errors_1.AppError("Your Activity log isn't turned on. Please ask OrbitXPro support to turn it on.", _http_1.HTTP.FORBIDDEN));
|
|
43
|
+
}
|
|
44
|
+
const q = req.query;
|
|
45
|
+
const page = Math.max(1, Number(q.page) || 1);
|
|
46
|
+
const limit = Math.min(200, Math.max(1, Number(q.limit) || 25));
|
|
47
|
+
const filter = { [field]: new mongoose_1.default.Types.ObjectId(String(account._id)) };
|
|
48
|
+
const actions = list(q.action);
|
|
49
|
+
if (actions.length)
|
|
50
|
+
filter.action = { $in: actions };
|
|
51
|
+
const outcomes = list(q.outcome).map((o) => o.toLowerCase());
|
|
52
|
+
if (outcomes.length)
|
|
53
|
+
filter.outcome = { $in: outcomes };
|
|
54
|
+
if (q.from || q.to) {
|
|
55
|
+
filter.at = {};
|
|
56
|
+
if (q.from)
|
|
57
|
+
filter.at.$gte = new Date(q.from);
|
|
58
|
+
if (q.to)
|
|
59
|
+
filter.at.$lte = new Date(q.to);
|
|
60
|
+
}
|
|
61
|
+
const and = [];
|
|
62
|
+
if (q.user)
|
|
63
|
+
and.push({ $or: ["actor.name", "actor.email"].map((f) => ({ [f]: { $regex: escape(String(q.user)), $options: "i" } })) });
|
|
64
|
+
if (q.details)
|
|
65
|
+
and.push({ details: { $regex: escape(String(q.details)), $options: "i" } });
|
|
66
|
+
if (q.search) {
|
|
67
|
+
const re = { $regex: escape(String(q.search)), $options: "i" };
|
|
68
|
+
and.push({ $or: searchFields.map((f) => ({ [f]: re })) });
|
|
69
|
+
}
|
|
70
|
+
if (and.length)
|
|
71
|
+
filter.$and = and;
|
|
72
|
+
const [rows, total] = await Promise.all([
|
|
73
|
+
ActivityLogModel.find(filter)
|
|
74
|
+
.sort({ at: -1 })
|
|
75
|
+
.skip((page - 1) * limit)
|
|
76
|
+
.limit(limit)
|
|
77
|
+
.select("at actor dashboard action details record outcome sensitive changes ip")
|
|
78
|
+
.lean(),
|
|
79
|
+
ActivityLogModel.countDocuments(filter),
|
|
80
|
+
]);
|
|
81
|
+
// OrbitXPro staff show by name and team; their work email and network address stay internal.
|
|
82
|
+
const items = rows.map((r) => {
|
|
83
|
+
var _a, _b, _c;
|
|
84
|
+
return ((_a = r.actor) === null || _a === void 0 ? void 0 : _a.kind) === kind
|
|
85
|
+
? r
|
|
86
|
+
: { ...r, actor: { kind: (_b = r.actor) === null || _b === void 0 ? void 0 : _b.kind, name: ((_c = r.actor) === null || _c === void 0 ? void 0 : _c.name) || "OrbitXPro", team: "OrbitXPro" }, ip: undefined };
|
|
87
|
+
});
|
|
88
|
+
res.status(_http_1.HTTP.OK).json({
|
|
89
|
+
success: true,
|
|
90
|
+
data: { items },
|
|
91
|
+
pagination: { page, limit, total, totalPages: Math.max(1, Math.ceil(total / limit)) },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const recordExport = (0, _http_1.catchAsync)(async (req, res) => {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
const what = String(((_a = req.body) === null || _a === void 0 ? void 0 : _a.what) || "a table").slice(0, 120);
|
|
97
|
+
const rows = Number((_b = req.body) === null || _b === void 0 ? void 0 : _b.rows);
|
|
98
|
+
(0, recorder_1.recordActivity)({
|
|
99
|
+
actor: { kind, id: req.user._id, name: req.user.name, email: req.user.email, role: req.user.role },
|
|
100
|
+
dashboard: kind,
|
|
101
|
+
action: "Exported",
|
|
102
|
+
details: `Exported ${what}${Number.isFinite(rows) ? ` (CSV, ${rows} row${rows === 1 ? "" : "s"})` : ""}`,
|
|
103
|
+
[field]: accountIdOf(req),
|
|
104
|
+
sensitive: true,
|
|
105
|
+
ip: (0, recorder_1.clientIp)(req),
|
|
106
|
+
userAgent: req.headers["user-agent"],
|
|
107
|
+
method: req.method,
|
|
108
|
+
path: req.originalUrl.split("?")[0],
|
|
109
|
+
status: 200,
|
|
110
|
+
});
|
|
111
|
+
res.status(_http_1.HTTP.OK).json({ success: true });
|
|
112
|
+
});
|
|
113
|
+
return { getAccess, listActivity, recordExport };
|
|
114
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function createOwnTicketsController({ ownerType, categoryOptions, fallbackAuthorName, uploadAttachments, raiseDispute }: {
|
|
2
|
+
ownerType: "Company" | "Partner";
|
|
3
|
+
categoryOptions: any[];
|
|
4
|
+
fallbackAuthorName: string;
|
|
5
|
+
uploadAttachments: (files: any[], ownerId: any) => Promise<any[]>;
|
|
6
|
+
/** Client only: opens the financial dispute behind a lead-dispute ticket. */
|
|
7
|
+
raiseDispute?: (input: {
|
|
8
|
+
leadId: any;
|
|
9
|
+
reason: string;
|
|
10
|
+
companyId: any;
|
|
11
|
+
raisedBy: any;
|
|
12
|
+
ticketId: any;
|
|
13
|
+
}) => Promise<any>;
|
|
14
|
+
}): {
|
|
15
|
+
getTicketCategories: (req: any, res: any, next: any) => void;
|
|
16
|
+
createTicket: (req: any, res: any, next: any) => void;
|
|
17
|
+
getMyTickets: (req: any, res: any, next: any) => void;
|
|
18
|
+
getMyTicketById: (req: any, res: any, next: any) => void;
|
|
19
|
+
addTicketMessage: (req: any, res: any, next: any) => void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=ownTickets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownTickets.d.ts","sourceRoot":"","sources":["../../src/helpers/ownTickets.ts"],"names":[],"mappings":"AAuBA,wBAAgB,0BAA0B,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE;IAC9H,SAAS,EAAE,SAAS,GAAG,SAAS,CAAC;IACjC,eAAe,EAAE,GAAG,EAAE,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAClE,6EAA6E;IAC7E,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,GAAG,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACvH;;;;;;EAoJA"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createOwnTicketsController = createOwnTicketsController;
|
|
7
|
+
/**
|
|
8
|
+
* A portal's own support tickets: category list, submit (auto-routed to Ops at once), list, view, reply. Shared by
|
|
9
|
+
* the Client and Partner backends:
|
|
10
|
+
* createOwnTicketsController({ ownerType: "Company", categoryOptions: CLIENT_TICKET_CATEGORY_OPTIONS,
|
|
11
|
+
* fallbackAuthorName: "Client", uploadAttachments, raiseDispute }) // Client: lead disputes too
|
|
12
|
+
* createOwnTicketsController({ ownerType: "Partner", categoryOptions: PARTNER_TICKET_CATEGORY_OPTIONS,
|
|
13
|
+
* fallbackAuthorName: "Partner", uploadAttachments })
|
|
14
|
+
* uploadAttachments(files, ownerId) stores the request's files and resolves the ticket attachment list.
|
|
15
|
+
*/
|
|
16
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
17
|
+
const ticket_model_1 = require("../models/ticket.model");
|
|
18
|
+
const update_model_1 = require("../models/update.model");
|
|
19
|
+
const ticketCategoryOverride_model_1 = require("../models/ticketCategoryOverride.model");
|
|
20
|
+
const ticketCategories_1 = require("../constants/ticketCategories");
|
|
21
|
+
const ticketRouting_1 = require("../routing/ticketRouting");
|
|
22
|
+
const ticketSanitize_1 = require("../utils/ticketSanitize");
|
|
23
|
+
const errors_1 = require("./errors");
|
|
24
|
+
const _http_1 = require("./_http");
|
|
25
|
+
// Categories where a client disputes one lead/chat: the only ones with a real LeadDispute behind them.
|
|
26
|
+
// "chat-dispute" is a legacy value (no longer offered) kept so an old ticket behaves as it always did.
|
|
27
|
+
const LEAD_DISPUTE_CATEGORIES = ["chat-dispute", "dispute-billable-lead"];
|
|
28
|
+
function createOwnTicketsController({ ownerType, categoryOptions, fallbackAuthorName, uploadAttachments, raiseDispute }) {
|
|
29
|
+
const TicketModel = ticket_model_1.Ticket;
|
|
30
|
+
const UpdateModel = update_model_1.Update;
|
|
31
|
+
const OverrideModel = ticketCategoryOverride_model_1.TicketSubCategoryOverride;
|
|
32
|
+
const VALID_CATEGORY_VALUES = new Set(categoryOptions.map((o) => o.value));
|
|
33
|
+
// The effective category list: the defaults plus any sub-categories Ops set in Settings.
|
|
34
|
+
const getTicketCategories = (0, _http_1.catchAsync)(async (req, res) => {
|
|
35
|
+
const overrides = await OverrideModel.find({ ownerType }).lean();
|
|
36
|
+
res.status(_http_1.HTTP.OK).json({ success: true, data: { categories: (0, ticketCategories_1.applySubCategoryOverrides)(categoryOptions, overrides) } });
|
|
37
|
+
});
|
|
38
|
+
// Fire-and-forget: a missed feed notification never fails the ticket creation that triggered it.
|
|
39
|
+
async function notifyAssignee(userId, ticket) {
|
|
40
|
+
if (!userId)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
await UpdateModel.create({
|
|
44
|
+
dashboard: "ops",
|
|
45
|
+
audience: "user",
|
|
46
|
+
userId,
|
|
47
|
+
type: "ticket_assigned",
|
|
48
|
+
title: `Ticket ${ticket.ticketId} assigned to you`,
|
|
49
|
+
body: ticket.description ? ticket.description.slice(0, 200) : undefined,
|
|
50
|
+
link: `/tickets/view/${ticket._id}`,
|
|
51
|
+
readBy: [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`[Ticket ${ticket._id}] Failed to write assignment update:`, err.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Routes a new ticket the same way Ops's own triage does. Routing is a convenience: an unrouted ticket just waits
|
|
59
|
+
// for Ops to pick it up.
|
|
60
|
+
async function routeNewTicket(ticket) {
|
|
61
|
+
try {
|
|
62
|
+
const routing = await (0, ticketRouting_1.resolveTicketRouting)(ticket);
|
|
63
|
+
if (!routing.assigneeId && routing.followerIds.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
const update = { $push: {} };
|
|
66
|
+
if (routing.assigneeId) {
|
|
67
|
+
update.$set = { assigneeId: routing.assigneeId };
|
|
68
|
+
update.$push.activityLog = { action: "assigned", actorName: "Routing Engine", to: String(routing.assigneeId), createdAt: new Date() };
|
|
69
|
+
}
|
|
70
|
+
if (routing.followerIds.length)
|
|
71
|
+
update.$push.followerIds = { $each: routing.followerIds };
|
|
72
|
+
await TicketModel.updateOne({ _id: ticket._id }, update);
|
|
73
|
+
await notifyAssignee(routing.assigneeId, ticket);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(`[Ticket ${ticket._id}] Auto-routing failed:`, err.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Body (multipart): category, subCategory, description, disputeReason, chatId, invoiceId (+ leadId on Client).
|
|
80
|
+
// File field: attachments (optional, several).
|
|
81
|
+
const createTicket = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
82
|
+
const { category, subCategory, description, disputeReason, chatId, invoiceId } = req.body;
|
|
83
|
+
const leadId = raiseDispute ? req.body.leadId : undefined;
|
|
84
|
+
const ownerId = req.user.owner;
|
|
85
|
+
if (!category)
|
|
86
|
+
return next(new errors_1.AppError("category is required", _http_1.HTTP.BAD_REQUEST));
|
|
87
|
+
if (!VALID_CATEGORY_VALUES.has(category))
|
|
88
|
+
return next(new errors_1.AppError("Invalid category", _http_1.HTTP.BAD_REQUEST));
|
|
89
|
+
// A sub-category an admin added from Settings must be accepted here, not just displayed.
|
|
90
|
+
const override = await OverrideModel.findOne({ ownerType, categoryValue: category }).lean();
|
|
91
|
+
const categoryDef = (0, ticketCategories_1.applySubCategoryOverrides)(categoryOptions, override ? [override] : []).find((o) => o.value === category);
|
|
92
|
+
if (subCategory && (categoryDef === null || categoryDef === void 0 ? void 0 : categoryDef.subCategories) && !categoryDef.subCategories.some((s) => s.value === subCategory)) {
|
|
93
|
+
return next(new errors_1.AppError("Invalid sub-category for this category", _http_1.HTTP.BAD_REQUEST));
|
|
94
|
+
}
|
|
95
|
+
const attachments = await uploadAttachments(req.files || [], ownerId);
|
|
96
|
+
const ticket = await TicketModel.create({
|
|
97
|
+
ownerType,
|
|
98
|
+
ownerId,
|
|
99
|
+
source: "dashboard",
|
|
100
|
+
category,
|
|
101
|
+
subCategory: subCategory || undefined,
|
|
102
|
+
description,
|
|
103
|
+
disputeReason,
|
|
104
|
+
chatId,
|
|
105
|
+
leadId: leadId && mongoose_1.default.Types.ObjectId.isValid(leadId) ? leadId : undefined,
|
|
106
|
+
invoiceId,
|
|
107
|
+
attachments,
|
|
108
|
+
requestedBy: req.user._id,
|
|
109
|
+
requesterEmail: req.user.email,
|
|
110
|
+
});
|
|
111
|
+
// A real lead dispute also opens the financial dispute Ops resolves. Never fails the ticket itself.
|
|
112
|
+
let disputeWarning;
|
|
113
|
+
if (raiseDispute && LEAD_DISPUTE_CATEGORIES.includes(category) && leadId) {
|
|
114
|
+
try {
|
|
115
|
+
await raiseDispute({ leadId, reason: disputeReason || description, companyId: ownerId, raisedBy: req.user._id, ticketId: ticket._id });
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[Ticket ${ticket._id}] Failed to create linked dispute for lead ${leadId}:`, err.message);
|
|
119
|
+
disputeWarning = err.message;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await routeNewTicket(ticket);
|
|
123
|
+
res.status(_http_1.HTTP.CREATED).json({ success: true, message: "Ticket submitted successfully", data: { ticket, disputeWarning } });
|
|
124
|
+
});
|
|
125
|
+
const withPeople = (query) => query.populate("requestedBy", "name email").populate("resolvedBy", "name email");
|
|
126
|
+
const getMyTickets = (0, _http_1.catchAsync)(async (req, res) => {
|
|
127
|
+
const { status, category } = req.query;
|
|
128
|
+
const queryObj = { ownerType, ownerId: req.user.owner };
|
|
129
|
+
if (status)
|
|
130
|
+
queryObj.status = status;
|
|
131
|
+
if (category)
|
|
132
|
+
queryObj.category = category;
|
|
133
|
+
const tickets = await withPeople(TicketModel.find(queryObj)).sort({ createdAt: -1 }).lean();
|
|
134
|
+
tickets.forEach(ticketSanitize_1.sanitizeTicketForRequester);
|
|
135
|
+
res.status(_http_1.HTTP.OK).json({ success: true, results: tickets.length, data: { tickets } });
|
|
136
|
+
});
|
|
137
|
+
const getMyTicketById = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
138
|
+
const ticket = await withPeople(TicketModel.findOne({ _id: req.params.id, ownerType, ownerId: req.user.owner })).lean();
|
|
139
|
+
if (!ticket)
|
|
140
|
+
return next(new errors_1.AppError("Ticket not found", _http_1.HTTP.NOT_FOUND));
|
|
141
|
+
res.status(_http_1.HTTP.OK).json({ success: true, data: { ticket: (0, ticketSanitize_1.sanitizeTicketForRequester)(ticket) } });
|
|
142
|
+
});
|
|
143
|
+
// A reply from the requester: always visible to them (they can never post an internal note).
|
|
144
|
+
// Body: { body }. File field: attachments (optional, several).
|
|
145
|
+
const addTicketMessage = (0, _http_1.catchAsync)(async (req, res, next) => {
|
|
146
|
+
const ownerId = req.user.owner;
|
|
147
|
+
const { body } = req.body;
|
|
148
|
+
if (!body || !body.trim())
|
|
149
|
+
return next(new errors_1.AppError("Message body is required", _http_1.HTTP.BAD_REQUEST));
|
|
150
|
+
const ticket = await TicketModel.findOne({ _id: req.params.id, ownerType, ownerId });
|
|
151
|
+
if (!ticket)
|
|
152
|
+
return next(new errors_1.AppError("Ticket not found", _http_1.HTTP.NOT_FOUND));
|
|
153
|
+
const attachments = await uploadAttachments(req.files || [], ownerId);
|
|
154
|
+
const message = {
|
|
155
|
+
_id: new mongoose_1.default.Types.ObjectId(),
|
|
156
|
+
authorType: "client",
|
|
157
|
+
authorId: req.user._id,
|
|
158
|
+
authorName: req.user.name || req.user.email || fallbackAuthorName,
|
|
159
|
+
body: body.trim(),
|
|
160
|
+
isInternal: false,
|
|
161
|
+
attachments,
|
|
162
|
+
createdAt: new Date(),
|
|
163
|
+
};
|
|
164
|
+
await TicketModel.updateOne({ _id: ticket._id }, { $push: { messages: message }, $set: { updatedAt: new Date() } });
|
|
165
|
+
const updated = await withPeople(TicketModel.findById(ticket._id)).lean();
|
|
166
|
+
res.status(_http_1.HTTP.CREATED).json({ success: true, message: "Message added", data: { ticket: (0, ticketSanitize_1.sanitizeTicketForRequester)(updated) } });
|
|
167
|
+
});
|
|
168
|
+
return { getTicketCategories, createTicket, getMyTickets, getMyTicketById, addTicketMessage };
|
|
169
|
+
}
|