@managemint-solutions/sdk 0.19.0 → 0.20.1
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/client.d.ts +2 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.js +17 -2
- package/dist/notifications/index.js.map +1 -1
- package/dist/service-client/index.d.ts +2 -0
- package/dist/service-client/index.js +7 -0
- package/dist/service-client/index.js.map +1 -1
- package/dist/support/tickets/dto.d.ts +33 -0
- package/dist/support/tickets/dto.js +141 -0
- package/dist/support/tickets/dto.js.map +1 -0
- package/dist/support/tickets/index.d.ts +161 -0
- package/dist/support/tickets/index.js +443 -0
- package/dist/support/tickets/index.js.map +1 -0
- package/dist/users/dto.d.ts +27 -1
- package/dist/users/dto.js +117 -1
- package/dist/users/dto.js.map +1 -1
- package/dist/users/index.d.ts +13 -0
- package/dist/users/index.js +32 -0
- package/dist/users/index.js.map +1 -1
- package/dist/users/paths.d.ts +24 -0
- package/dist/users/paths.js +39 -0
- package/dist/users/paths.js.map +1 -0
- package/package.json +8 -3
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { SupportAttachment, SupportMessageEntity, SupportTicketEntity, SupportTicketSummary } from '@managemint-solutions/entities/support/tickets';
|
|
2
|
+
import { SupportMessageSource, SupportTicketStatus } from '@managemint-solutions/entities/support/tickets/enum';
|
|
3
|
+
import type { AuthContext } from '../../auth';
|
|
4
|
+
import { type AuditedTable, type AuditedTableOptions, type AuditResource, type AuditRow } from '../../audit';
|
|
5
|
+
import type { TypedSupabaseClient } from '../../client';
|
|
6
|
+
import type { CreateSupportTicketDto, GetSupportTicketsDto, ReplySupportTicketDto } from './dto';
|
|
7
|
+
export * from './dto';
|
|
8
|
+
/**
|
|
9
|
+
* A member's own tickets. Every read and write is scoped by `organization_id`, matching the RLS
|
|
10
|
+
* policies, and both tables are audited: `support_tickets` for the ticket and its status moves,
|
|
11
|
+
* `support_messages` for the thread.
|
|
12
|
+
*
|
|
13
|
+
* The write payloads are deliberately narrow. Members hold an INSERT grant on `support_messages`
|
|
14
|
+
* that omits `author_type`/`author_email`/`source`/`email_message_id` — the column defaults do
|
|
15
|
+
* that work, so a member cannot forge an agent message — and an UPDATE grant on
|
|
16
|
+
* `support_tickets` of `(status, updated_at)` only, which is why assignment, resolution and
|
|
17
|
+
* closure live on the admin resource instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SupportTicketsResource {
|
|
20
|
+
private readonly supabase;
|
|
21
|
+
private readonly auth;
|
|
22
|
+
private readonly ticketsTable;
|
|
23
|
+
private readonly messagesTable;
|
|
24
|
+
constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
|
|
25
|
+
list(query: GetSupportTicketsDto): Promise<SupportTicketSummary[]>;
|
|
26
|
+
getById(ticketId: string): Promise<SupportTicketEntity>;
|
|
27
|
+
/** The thread, oldest first — it is a conversation, and it is append-only. */
|
|
28
|
+
messages(ticketId: string): Promise<SupportMessageEntity[]>;
|
|
29
|
+
create(input: CreateSupportTicketDto): Promise<SupportTicketEntity>;
|
|
30
|
+
/**
|
|
31
|
+
* A client's reply. The status is read first because a closed ticket must be refused outright
|
|
32
|
+
* — no row written, a 400 back — rather than silently swallowed by the compare-and-set below.
|
|
33
|
+
*
|
|
34
|
+
* Everything else moves the ticket to `awaiting_agent`, including `resolved`, which is what
|
|
35
|
+
* reopens it. The move is guarded so a reply that races the agent closing the ticket loses
|
|
36
|
+
* cleanly: the message stands, the status does not move.
|
|
37
|
+
*/
|
|
38
|
+
reply(ticketId: string, input: ReplySupportTicketDto): Promise<SupportMessageEntity>;
|
|
39
|
+
/** The just-written message, re-read so the caller gets the entity shape, embed and all. */
|
|
40
|
+
private messageById;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* How the admin resource reaches an audited table once the row has told it which organization —
|
|
44
|
+
* and, where there is one, which user — the write belongs to. The same shape as the outbox's
|
|
45
|
+
* `OrganizationTableFactory`, and for the same reason: the resource spans organizations, so it
|
|
46
|
+
* cannot be built inside one.
|
|
47
|
+
*/
|
|
48
|
+
export type SupportAuditedTableFactory = (organizationId: string, actorMmsId: string | null, options: AuditedTableOptions) => AuditedTable;
|
|
49
|
+
/** A ticket as the admin side reads it — tenancy and the reply token included. */
|
|
50
|
+
export type SupportTicketWithSecrets = {
|
|
51
|
+
mms_id: string;
|
|
52
|
+
organization_id: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
created_by: string | null;
|
|
55
|
+
subject: string;
|
|
56
|
+
description: string;
|
|
57
|
+
status: SupportTicketStatus;
|
|
58
|
+
priority: string;
|
|
59
|
+
category: string;
|
|
60
|
+
assigned_to: string | null;
|
|
61
|
+
deleted: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The secret behind `support+<token>@…`, from the ticket's row in
|
|
64
|
+
* `support_ticket_email_tokens`. `null` when the ticket has no token row — a ticket whose
|
|
65
|
+
* token insert never landed can still be worked on, it just has no replyable address.
|
|
66
|
+
*/
|
|
67
|
+
email_token: string | null;
|
|
68
|
+
};
|
|
69
|
+
/** A cross-organization row of `listAll` — the summary plus the tenant it belongs to. */
|
|
70
|
+
export type SupportTicketAdminSummary = SupportTicketSummary & {
|
|
71
|
+
organization_id: string;
|
|
72
|
+
};
|
|
73
|
+
/** What the agent side supplies when it writes into a thread. */
|
|
74
|
+
export type SupportAgentMessageInput = {
|
|
75
|
+
body: string;
|
|
76
|
+
author_email: string | null;
|
|
77
|
+
source: SupportMessageSource;
|
|
78
|
+
email_message_id?: string | null;
|
|
79
|
+
attachments?: SupportAttachment[] | null;
|
|
80
|
+
};
|
|
81
|
+
/** What an inbound client email supplies. Its source is always email, so it is not asked for. */
|
|
82
|
+
export type SupportInboundMessageInput = {
|
|
83
|
+
body: string;
|
|
84
|
+
author_email: string | null;
|
|
85
|
+
email_message_id?: string | null;
|
|
86
|
+
attachments?: SupportAttachment[] | null;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The agent's side of support, on the service client: the inbound email webhook, which knows only
|
|
90
|
+
* a reply token, and the shared-secret admin routes. RLS reserves everything here to the service
|
|
91
|
+
* role — `assigned_to`, `resolved_at`, `closed_at` and the agent columns on a message are all
|
|
92
|
+
* outside a member's grants, and `support_ticket_email_tokens` is outside every member's grant
|
|
93
|
+
* entirely.
|
|
94
|
+
*
|
|
95
|
+
* Reads that answer "which tenant is this?" are unscoped by necessity; every write is attributed
|
|
96
|
+
* to the row's own organization, with the actor being the matched user for an inbound client mail
|
|
97
|
+
* and `null` for agent and system work.
|
|
98
|
+
*/
|
|
99
|
+
export declare class SupportTicketsAdminResource {
|
|
100
|
+
private readonly supabase;
|
|
101
|
+
private readonly tableFor;
|
|
102
|
+
constructor(supabase: TypedSupabaseClient, tableFor: SupportAuditedTableFactory);
|
|
103
|
+
private tickets;
|
|
104
|
+
private messages;
|
|
105
|
+
/**
|
|
106
|
+
* Every organization's tickets in one list. Deliberately unscoped: the sole support agent works
|
|
107
|
+
* across tenants, and this read is what the service role exists for.
|
|
108
|
+
*/
|
|
109
|
+
listAll(): Promise<SupportTicketAdminSummary[]>;
|
|
110
|
+
/**
|
|
111
|
+
* The ticket behind a reply address. Read from the token table, which is the side that holds
|
|
112
|
+
* the foreign key, so the ticket arrives embedded and one query answers the whole question.
|
|
113
|
+
* Also unscoped by necessity — an inbound email carries nothing but the token, and the row it
|
|
114
|
+
* matches is what says which tenant it belongs to. No match means the mail was never ticket
|
|
115
|
+
* mail, and neither is a soft-deleted ticket.
|
|
116
|
+
*/
|
|
117
|
+
findByEmailToken(token: string): Promise<SupportTicketWithSecrets | null>;
|
|
118
|
+
/**
|
|
119
|
+
* The same shape by id, for the admin routes, which already know the ticket they are acting on.
|
|
120
|
+
* The ticket is read first and on its own: an admin must be able to work a ticket whose token
|
|
121
|
+
* row is missing, so the token is looked up separately and its absence is a `null`, not a 404.
|
|
122
|
+
*/
|
|
123
|
+
getWithSecrets(ticketId: string): Promise<SupportTicketWithSecrets>;
|
|
124
|
+
/**
|
|
125
|
+
* Mints the ticket's reply token. The api calls it as soon as a member's ticket is created,
|
|
126
|
+
* and the address `support+<token>@…` is built from what comes back.
|
|
127
|
+
*
|
|
128
|
+
* A plain insert, deliberately unaudited: there is no `AuditTrailEntityType` for the table, and
|
|
129
|
+
* `audit_logs` is readable by any member holding `read_logs` — so a token snapshotted into a
|
|
130
|
+
* change set would be the one leak the separate table exists to prevent.
|
|
131
|
+
*/
|
|
132
|
+
createEmailToken(ticketId: string): Promise<string>;
|
|
133
|
+
private emailToken;
|
|
134
|
+
/**
|
|
135
|
+
* An agent's reply, from the mail Scott sends or from the admin route. The ball goes back to
|
|
136
|
+
* the client — but only from a live status, so writing on a resolved or closed ticket leaves it
|
|
137
|
+
* where it is.
|
|
138
|
+
*/
|
|
139
|
+
addAgentMessage(ticket: SupportTicketWithSecrets, message: SupportAgentMessageInput): Promise<AuditRow>;
|
|
140
|
+
/**
|
|
141
|
+
* A client's reply that arrived by email. `userMmsId` is the profile the From header was matched
|
|
142
|
+
* to — the classification the webhook has already done — so the message and the status move are
|
|
143
|
+
* attributed to them exactly as a portal reply would be, reopening a resolved ticket.
|
|
144
|
+
*/
|
|
145
|
+
addInboundUserMessage(ticket: SupportTicketWithSecrets, userMmsId: string, message: SupportInboundMessageInput): Promise<AuditRow>;
|
|
146
|
+
/**
|
|
147
|
+
* Puts a ticket on a staff member. Works for a first assign and a reassign alike, and refuses
|
|
148
|
+
* on a closed ticket — the guard is `status <> closed`, so losing that race writes nothing and
|
|
149
|
+
* returns null.
|
|
150
|
+
*
|
|
151
|
+
* A first assign additionally puts the ticket in the agent's queue, but only from `open`: a
|
|
152
|
+
* thread that has already moved on keeps the status it earned, and a reassign never touches
|
|
153
|
+
* status at all. That flip is its own compare-and-set rather than part of the patch above, so
|
|
154
|
+
* a status that moves between the two never gets overwritten.
|
|
155
|
+
*/
|
|
156
|
+
assign(ticketId: string, staffMmsId: string): Promise<AuditRow | null>;
|
|
157
|
+
/** Resolves a live ticket. Concurrent resolves settle on one write; the loser returns null. */
|
|
158
|
+
resolve(ticketId: string): Promise<AuditRow | null>;
|
|
159
|
+
/** Closes a ticket from any status but closed. Terminal — nothing reopens it. */
|
|
160
|
+
close(ticketId: string): Promise<AuditRow | null>;
|
|
161
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.SupportTicketsAdminResource = exports.SupportTicketsResource = void 0;
|
|
18
|
+
const enum_1 = require("@managemint-solutions/entities/support/tickets/enum");
|
|
19
|
+
const enum_2 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
20
|
+
const audit_1 = require("../../audit");
|
|
21
|
+
const errors_1 = require("../../errors");
|
|
22
|
+
const transforms_1 = require("../../transforms");
|
|
23
|
+
__exportStar(require("./dto"), exports);
|
|
24
|
+
const SUPPORT_TICKETS_TABLE = {
|
|
25
|
+
table: 'support_tickets',
|
|
26
|
+
entityType: enum_2.AuditTrailEntityType.SUPPORT_TICKETS,
|
|
27
|
+
};
|
|
28
|
+
const SUPPORT_MESSAGES_TABLE = {
|
|
29
|
+
table: 'support_messages',
|
|
30
|
+
entityType: enum_2.AuditTrailEntityType.SUPPORT_MESSAGES,
|
|
31
|
+
};
|
|
32
|
+
// The public contract is the shared entities types; the casts at the returns declare the shape
|
|
33
|
+
// these select strings produce (the columns are defined in the api's supabase/migrations).
|
|
34
|
+
// `staff` is embedded through the `assigned_to` column, the same way every other embed here
|
|
35
|
+
// names its foreign key column rather than the constraint behind it.
|
|
36
|
+
const STAFF_EMBED = `(
|
|
37
|
+
mms_id,
|
|
38
|
+
name,
|
|
39
|
+
surname
|
|
40
|
+
)`;
|
|
41
|
+
const SUPPORT_TICKET_LIST_SELECT = `
|
|
42
|
+
mms_id,
|
|
43
|
+
created_at,
|
|
44
|
+
updated_at,
|
|
45
|
+
subject,
|
|
46
|
+
status,
|
|
47
|
+
priority,
|
|
48
|
+
category,
|
|
49
|
+
assigned_to_staff:staff!assigned_to ${STAFF_EMBED}
|
|
50
|
+
`;
|
|
51
|
+
const SUPPORT_TICKET_DETAIL_SELECT = `
|
|
52
|
+
mms_id,
|
|
53
|
+
created_at,
|
|
54
|
+
created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
|
|
55
|
+
updated_at,
|
|
56
|
+
subject,
|
|
57
|
+
description,
|
|
58
|
+
status,
|
|
59
|
+
priority,
|
|
60
|
+
category,
|
|
61
|
+
assigned_to_staff:staff!assigned_to ${STAFF_EMBED},
|
|
62
|
+
attachments,
|
|
63
|
+
resolved_at,
|
|
64
|
+
closed_at
|
|
65
|
+
`;
|
|
66
|
+
const SUPPORT_MESSAGE_SELECT = `
|
|
67
|
+
mms_id,
|
|
68
|
+
created_at,
|
|
69
|
+
created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
|
|
70
|
+
author_type,
|
|
71
|
+
author_email,
|
|
72
|
+
source,
|
|
73
|
+
body,
|
|
74
|
+
attachments
|
|
75
|
+
`;
|
|
76
|
+
/**
|
|
77
|
+
* The statuses a client reply may move a ticket out of — every one but `closed`, which is
|
|
78
|
+
* terminal. `resolved` is deliberately in the list: a reply reopens a resolved ticket.
|
|
79
|
+
*/
|
|
80
|
+
const CLIENT_REPLY_FROM = [
|
|
81
|
+
enum_1.SupportTicketStatus.OPEN,
|
|
82
|
+
enum_1.SupportTicketStatus.AWAITING_AGENT,
|
|
83
|
+
enum_1.SupportTicketStatus.AWAITING_USER,
|
|
84
|
+
enum_1.SupportTicketStatus.RESOLVED,
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* The statuses an agent reply may move a ticket out of. `resolved` is absent — an agent writing
|
|
88
|
+
* on a resolved ticket is a parting note, not a reason to put the ball back in the client's court.
|
|
89
|
+
*/
|
|
90
|
+
const AGENT_REPLY_FROM = [
|
|
91
|
+
enum_1.SupportTicketStatus.OPEN,
|
|
92
|
+
enum_1.SupportTicketStatus.AWAITING_AGENT,
|
|
93
|
+
enum_1.SupportTicketStatus.AWAITING_USER,
|
|
94
|
+
];
|
|
95
|
+
/** The statuses `resolve` may move a ticket out of — anything still live. */
|
|
96
|
+
const RESOLVABLE_FROM = [
|
|
97
|
+
enum_1.SupportTicketStatus.OPEN,
|
|
98
|
+
enum_1.SupportTicketStatus.AWAITING_AGENT,
|
|
99
|
+
enum_1.SupportTicketStatus.AWAITING_USER,
|
|
100
|
+
];
|
|
101
|
+
/** The 400 a portal or admin reply gets when the thread is over. */
|
|
102
|
+
const ticketClosed = () => new errors_1.SupabaseClientError({
|
|
103
|
+
status: 400,
|
|
104
|
+
error: 'Ticket Closed',
|
|
105
|
+
message: 'This ticket is closed and can no longer be replied to.',
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* A member's own tickets. Every read and write is scoped by `organization_id`, matching the RLS
|
|
109
|
+
* policies, and both tables are audited: `support_tickets` for the ticket and its status moves,
|
|
110
|
+
* `support_messages` for the thread.
|
|
111
|
+
*
|
|
112
|
+
* The write payloads are deliberately narrow. Members hold an INSERT grant on `support_messages`
|
|
113
|
+
* that omits `author_type`/`author_email`/`source`/`email_message_id` — the column defaults do
|
|
114
|
+
* that work, so a member cannot forge an agent message — and an UPDATE grant on
|
|
115
|
+
* `support_tickets` of `(status, updated_at)` only, which is why assignment, resolution and
|
|
116
|
+
* closure live on the admin resource instead.
|
|
117
|
+
*/
|
|
118
|
+
class SupportTicketsResource {
|
|
119
|
+
supabase;
|
|
120
|
+
auth;
|
|
121
|
+
ticketsTable;
|
|
122
|
+
messagesTable;
|
|
123
|
+
constructor(supabase, auth, audit) {
|
|
124
|
+
this.supabase = supabase;
|
|
125
|
+
this.auth = auth;
|
|
126
|
+
this.ticketsTable = (0, audit_1.auditedTable)(supabase, auth, audit, SUPPORT_TICKETS_TABLE);
|
|
127
|
+
this.messagesTable = (0, audit_1.auditedTable)(supabase, auth, audit, SUPPORT_MESSAGES_TABLE);
|
|
128
|
+
}
|
|
129
|
+
async list(query) {
|
|
130
|
+
// `order` returns `this`, so the builder stays a filter builder and the conditional filter
|
|
131
|
+
// below can be reassigned onto it without a cast.
|
|
132
|
+
let ticketsQuery = this.supabase
|
|
133
|
+
.from('support_tickets')
|
|
134
|
+
.select(SUPPORT_TICKET_LIST_SELECT)
|
|
135
|
+
.eq('organization_id', this.auth.organization_id)
|
|
136
|
+
.eq('deleted', false)
|
|
137
|
+
.order('created_at', { ascending: false });
|
|
138
|
+
if (query.status) {
|
|
139
|
+
ticketsQuery = ticketsQuery.eq('status', query.status);
|
|
140
|
+
}
|
|
141
|
+
const { data, error } = await ticketsQuery;
|
|
142
|
+
if (error)
|
|
143
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
144
|
+
return data;
|
|
145
|
+
}
|
|
146
|
+
async getById(ticketId) {
|
|
147
|
+
const { data, error } = await this.supabase
|
|
148
|
+
.from('support_tickets')
|
|
149
|
+
.select(SUPPORT_TICKET_DETAIL_SELECT)
|
|
150
|
+
.eq('mms_id', ticketId)
|
|
151
|
+
.eq('organization_id', this.auth.organization_id)
|
|
152
|
+
.single();
|
|
153
|
+
if (error)
|
|
154
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
/** The thread, oldest first — it is a conversation, and it is append-only. */
|
|
158
|
+
async messages(ticketId) {
|
|
159
|
+
const { data, error } = await this.supabase
|
|
160
|
+
.from('support_messages')
|
|
161
|
+
.select(SUPPORT_MESSAGE_SELECT)
|
|
162
|
+
.eq('organization_id', this.auth.organization_id)
|
|
163
|
+
.eq('ticket_id', ticketId)
|
|
164
|
+
.order('created_at', { ascending: true });
|
|
165
|
+
if (error)
|
|
166
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
167
|
+
return data;
|
|
168
|
+
}
|
|
169
|
+
async create(input) {
|
|
170
|
+
// `status` is left out, and so are `priority`/`category` when the caller sent nothing: the
|
|
171
|
+
// column defaults (`open`, `normal`, `other`) are what a new ticket is meant to start as.
|
|
172
|
+
const row = await this.ticketsTable.insert({
|
|
173
|
+
subject: input.subject,
|
|
174
|
+
description: input.description,
|
|
175
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
176
|
+
...(input.category ? { category: input.category } : {}),
|
|
177
|
+
attachments: input.attachments ?? [],
|
|
178
|
+
organization_id: this.auth.organization_id,
|
|
179
|
+
created_by: this.auth.mms_id,
|
|
180
|
+
});
|
|
181
|
+
return this.getById(row.mms_id);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A client's reply. The status is read first because a closed ticket must be refused outright
|
|
185
|
+
* — no row written, a 400 back — rather than silently swallowed by the compare-and-set below.
|
|
186
|
+
*
|
|
187
|
+
* Everything else moves the ticket to `awaiting_agent`, including `resolved`, which is what
|
|
188
|
+
* reopens it. The move is guarded so a reply that races the agent closing the ticket loses
|
|
189
|
+
* cleanly: the message stands, the status does not move.
|
|
190
|
+
*/
|
|
191
|
+
async reply(ticketId, input) {
|
|
192
|
+
const { data, error } = await this.supabase
|
|
193
|
+
.from('support_tickets')
|
|
194
|
+
.select('mms_id, status')
|
|
195
|
+
.eq('mms_id', ticketId)
|
|
196
|
+
.eq('organization_id', this.auth.organization_id)
|
|
197
|
+
.single();
|
|
198
|
+
if (error)
|
|
199
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
200
|
+
if (data.status === enum_1.SupportTicketStatus.CLOSED) {
|
|
201
|
+
throw ticketClosed();
|
|
202
|
+
}
|
|
203
|
+
// Only the columns the member's INSERT grant covers — `author_type`, `source` and the rest
|
|
204
|
+
// are the table's defaults, not this caller's to state.
|
|
205
|
+
const row = await this.messagesTable.insert({
|
|
206
|
+
ticket_id: ticketId,
|
|
207
|
+
body: input.body,
|
|
208
|
+
attachments: input.attachments ?? [],
|
|
209
|
+
organization_id: this.auth.organization_id,
|
|
210
|
+
created_by: this.auth.mms_id,
|
|
211
|
+
});
|
|
212
|
+
await this.ticketsTable.updateWhere(ticketId, (query) => query.in('status', CLIENT_REPLY_FROM), {
|
|
213
|
+
status: enum_1.SupportTicketStatus.AWAITING_AGENT,
|
|
214
|
+
updated_at: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
return this.messageById(row.mms_id);
|
|
217
|
+
}
|
|
218
|
+
/** The just-written message, re-read so the caller gets the entity shape, embed and all. */
|
|
219
|
+
async messageById(messageId) {
|
|
220
|
+
const { data, error } = await this.supabase
|
|
221
|
+
.from('support_messages')
|
|
222
|
+
.select(SUPPORT_MESSAGE_SELECT)
|
|
223
|
+
.eq('mms_id', messageId)
|
|
224
|
+
.eq('organization_id', this.auth.organization_id)
|
|
225
|
+
.single();
|
|
226
|
+
if (error)
|
|
227
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.SupportTicketsResource = SupportTicketsResource;
|
|
232
|
+
/**
|
|
233
|
+
* The reply tokens, in their own table. Nothing here is granted to `authenticated` at all, which
|
|
234
|
+
* is what lets `support_tickets` keep full column grants — and so lets every member-side audited
|
|
235
|
+
* write, which pre-reads with `select('*')`, work at all.
|
|
236
|
+
*/
|
|
237
|
+
const SUPPORT_TICKET_EMAIL_TOKENS = 'support_ticket_email_tokens';
|
|
238
|
+
const SUPPORT_TICKET_SECRETS_SELECT = `
|
|
239
|
+
mms_id,
|
|
240
|
+
organization_id,
|
|
241
|
+
created_at,
|
|
242
|
+
created_by,
|
|
243
|
+
subject,
|
|
244
|
+
description,
|
|
245
|
+
status,
|
|
246
|
+
priority,
|
|
247
|
+
category,
|
|
248
|
+
assigned_to,
|
|
249
|
+
deleted
|
|
250
|
+
`;
|
|
251
|
+
/** A token row with the ticket it points at, read from the foreign key's own side. */
|
|
252
|
+
const SUPPORT_TICKET_BY_TOKEN_SELECT = `
|
|
253
|
+
email_token,
|
|
254
|
+
ticket:support_tickets!ticket_id (${SUPPORT_TICKET_SECRETS_SELECT})
|
|
255
|
+
`;
|
|
256
|
+
/**
|
|
257
|
+
* The agent's side of support, on the service client: the inbound email webhook, which knows only
|
|
258
|
+
* a reply token, and the shared-secret admin routes. RLS reserves everything here to the service
|
|
259
|
+
* role — `assigned_to`, `resolved_at`, `closed_at` and the agent columns on a message are all
|
|
260
|
+
* outside a member's grants, and `support_ticket_email_tokens` is outside every member's grant
|
|
261
|
+
* entirely.
|
|
262
|
+
*
|
|
263
|
+
* Reads that answer "which tenant is this?" are unscoped by necessity; every write is attributed
|
|
264
|
+
* to the row's own organization, with the actor being the matched user for an inbound client mail
|
|
265
|
+
* and `null` for agent and system work.
|
|
266
|
+
*/
|
|
267
|
+
class SupportTicketsAdminResource {
|
|
268
|
+
supabase;
|
|
269
|
+
tableFor;
|
|
270
|
+
constructor(supabase, tableFor) {
|
|
271
|
+
this.supabase = supabase;
|
|
272
|
+
this.tableFor = tableFor;
|
|
273
|
+
}
|
|
274
|
+
tickets(organizationId, actorMmsId = null) {
|
|
275
|
+
return this.tableFor(organizationId, actorMmsId, SUPPORT_TICKETS_TABLE);
|
|
276
|
+
}
|
|
277
|
+
messages(organizationId, actorMmsId = null) {
|
|
278
|
+
return this.tableFor(organizationId, actorMmsId, SUPPORT_MESSAGES_TABLE);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Every organization's tickets in one list. Deliberately unscoped: the sole support agent works
|
|
282
|
+
* across tenants, and this read is what the service role exists for.
|
|
283
|
+
*/
|
|
284
|
+
async listAll() {
|
|
285
|
+
const { data, error } = await this.supabase
|
|
286
|
+
.from('support_tickets')
|
|
287
|
+
.select(`organization_id, ${SUPPORT_TICKET_LIST_SELECT}`)
|
|
288
|
+
.eq('deleted', false)
|
|
289
|
+
.order('created_at', { ascending: false });
|
|
290
|
+
if (error)
|
|
291
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
292
|
+
return data;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The ticket behind a reply address. Read from the token table, which is the side that holds
|
|
296
|
+
* the foreign key, so the ticket arrives embedded and one query answers the whole question.
|
|
297
|
+
* Also unscoped by necessity — an inbound email carries nothing but the token, and the row it
|
|
298
|
+
* matches is what says which tenant it belongs to. No match means the mail was never ticket
|
|
299
|
+
* mail, and neither is a soft-deleted ticket.
|
|
300
|
+
*/
|
|
301
|
+
async findByEmailToken(token) {
|
|
302
|
+
const { data, error } = await this.supabase
|
|
303
|
+
.from(SUPPORT_TICKET_EMAIL_TOKENS)
|
|
304
|
+
.select(SUPPORT_TICKET_BY_TOKEN_SELECT)
|
|
305
|
+
.eq('email_token', token)
|
|
306
|
+
.maybeSingle();
|
|
307
|
+
if (error)
|
|
308
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
309
|
+
const row = (data ?? null);
|
|
310
|
+
if (!row?.ticket || row.ticket.deleted)
|
|
311
|
+
return null;
|
|
312
|
+
return { ...row.ticket, email_token: row.email_token };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The same shape by id, for the admin routes, which already know the ticket they are acting on.
|
|
316
|
+
* The ticket is read first and on its own: an admin must be able to work a ticket whose token
|
|
317
|
+
* row is missing, so the token is looked up separately and its absence is a `null`, not a 404.
|
|
318
|
+
*/
|
|
319
|
+
async getWithSecrets(ticketId) {
|
|
320
|
+
const { data, error } = await this.supabase
|
|
321
|
+
.from('support_tickets')
|
|
322
|
+
.select(SUPPORT_TICKET_SECRETS_SELECT)
|
|
323
|
+
.eq('mms_id', ticketId)
|
|
324
|
+
.single();
|
|
325
|
+
if (error)
|
|
326
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
327
|
+
const ticket = data;
|
|
328
|
+
return { ...ticket, email_token: await this.emailToken(ticketId) };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Mints the ticket's reply token. The api calls it as soon as a member's ticket is created,
|
|
332
|
+
* and the address `support+<token>@…` is built from what comes back.
|
|
333
|
+
*
|
|
334
|
+
* A plain insert, deliberately unaudited: there is no `AuditTrailEntityType` for the table, and
|
|
335
|
+
* `audit_logs` is readable by any member holding `read_logs` — so a token snapshotted into a
|
|
336
|
+
* change set would be the one leak the separate table exists to prevent.
|
|
337
|
+
*/
|
|
338
|
+
async createEmailToken(ticketId) {
|
|
339
|
+
const { data, error } = await this.supabase
|
|
340
|
+
.from(SUPPORT_TICKET_EMAIL_TOKENS)
|
|
341
|
+
.insert({ ticket_id: ticketId })
|
|
342
|
+
.select('email_token')
|
|
343
|
+
.single();
|
|
344
|
+
if (error)
|
|
345
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
346
|
+
return data.email_token;
|
|
347
|
+
}
|
|
348
|
+
async emailToken(ticketId) {
|
|
349
|
+
const { data, error } = await this.supabase
|
|
350
|
+
.from(SUPPORT_TICKET_EMAIL_TOKENS)
|
|
351
|
+
.select('email_token')
|
|
352
|
+
.eq('ticket_id', ticketId)
|
|
353
|
+
.maybeSingle();
|
|
354
|
+
if (error)
|
|
355
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
356
|
+
return data?.email_token ?? null;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* An agent's reply, from the mail Scott sends or from the admin route. The ball goes back to
|
|
360
|
+
* the client — but only from a live status, so writing on a resolved or closed ticket leaves it
|
|
361
|
+
* where it is.
|
|
362
|
+
*/
|
|
363
|
+
async addAgentMessage(ticket, message) {
|
|
364
|
+
// No user is behind agent work, so the audit actor stays null — `audit_logs.created_by` is
|
|
365
|
+
// nullable precisely for this.
|
|
366
|
+
const row = await this.messages(ticket.organization_id).insert({
|
|
367
|
+
ticket_id: ticket.mms_id,
|
|
368
|
+
organization_id: ticket.organization_id,
|
|
369
|
+
created_by: null,
|
|
370
|
+
author_type: enum_1.SupportMessageAuthorType.AGENT,
|
|
371
|
+
author_email: message.author_email,
|
|
372
|
+
source: message.source,
|
|
373
|
+
email_message_id: message.email_message_id ?? null,
|
|
374
|
+
body: message.body,
|
|
375
|
+
attachments: message.attachments ?? [],
|
|
376
|
+
});
|
|
377
|
+
await this.tickets(ticket.organization_id).updateWhere(ticket.mms_id, (query) => query.in('status', AGENT_REPLY_FROM), {
|
|
378
|
+
status: enum_1.SupportTicketStatus.AWAITING_USER,
|
|
379
|
+
updated_at: new Date().toISOString(),
|
|
380
|
+
});
|
|
381
|
+
return row;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* A client's reply that arrived by email. `userMmsId` is the profile the From header was matched
|
|
385
|
+
* to — the classification the webhook has already done — so the message and the status move are
|
|
386
|
+
* attributed to them exactly as a portal reply would be, reopening a resolved ticket.
|
|
387
|
+
*/
|
|
388
|
+
async addInboundUserMessage(ticket, userMmsId, message) {
|
|
389
|
+
const row = await this.messages(ticket.organization_id, userMmsId).insert({
|
|
390
|
+
ticket_id: ticket.mms_id,
|
|
391
|
+
organization_id: ticket.organization_id,
|
|
392
|
+
created_by: userMmsId,
|
|
393
|
+
author_type: enum_1.SupportMessageAuthorType.USER,
|
|
394
|
+
author_email: message.author_email,
|
|
395
|
+
source: enum_1.SupportMessageSource.EMAIL,
|
|
396
|
+
email_message_id: message.email_message_id ?? null,
|
|
397
|
+
body: message.body,
|
|
398
|
+
attachments: message.attachments ?? [],
|
|
399
|
+
});
|
|
400
|
+
await this.tickets(ticket.organization_id, userMmsId).updateWhere(ticket.mms_id, (query) => query.in('status', CLIENT_REPLY_FROM), {
|
|
401
|
+
status: enum_1.SupportTicketStatus.AWAITING_AGENT,
|
|
402
|
+
updated_at: new Date().toISOString(),
|
|
403
|
+
});
|
|
404
|
+
return row;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Puts a ticket on a staff member. Works for a first assign and a reassign alike, and refuses
|
|
408
|
+
* on a closed ticket — the guard is `status <> closed`, so losing that race writes nothing and
|
|
409
|
+
* returns null.
|
|
410
|
+
*
|
|
411
|
+
* A first assign additionally puts the ticket in the agent's queue, but only from `open`: a
|
|
412
|
+
* thread that has already moved on keeps the status it earned, and a reassign never touches
|
|
413
|
+
* status at all. That flip is its own compare-and-set rather than part of the patch above, so
|
|
414
|
+
* a status that moves between the two never gets overwritten.
|
|
415
|
+
*/
|
|
416
|
+
async assign(ticketId, staffMmsId) {
|
|
417
|
+
const ticket = await this.getWithSecrets(ticketId);
|
|
418
|
+
const firstAssign = ticket.assigned_to === null;
|
|
419
|
+
const table = this.tickets(ticket.organization_id);
|
|
420
|
+
const at = new Date().toISOString();
|
|
421
|
+
const assigned = await table.updateWhere(ticketId, (query) => query.neq('status', enum_1.SupportTicketStatus.CLOSED), { assigned_to: staffMmsId, updated_at: at });
|
|
422
|
+
if (!assigned)
|
|
423
|
+
return null;
|
|
424
|
+
if (!firstAssign)
|
|
425
|
+
return assigned;
|
|
426
|
+
const queued = await table.updateWhere(ticketId, (query) => query.eq('status', enum_1.SupportTicketStatus.OPEN), { status: enum_1.SupportTicketStatus.AWAITING_AGENT, updated_at: at });
|
|
427
|
+
return queued ?? assigned;
|
|
428
|
+
}
|
|
429
|
+
/** Resolves a live ticket. Concurrent resolves settle on one write; the loser returns null. */
|
|
430
|
+
async resolve(ticketId) {
|
|
431
|
+
const ticket = await this.getWithSecrets(ticketId);
|
|
432
|
+
const at = new Date().toISOString();
|
|
433
|
+
return this.tickets(ticket.organization_id).updateWhere(ticketId, (query) => query.in('status', RESOLVABLE_FROM), { status: enum_1.SupportTicketStatus.RESOLVED, resolved_at: at, updated_at: at });
|
|
434
|
+
}
|
|
435
|
+
/** Closes a ticket from any status but closed. Terminal — nothing reopens it. */
|
|
436
|
+
async close(ticketId) {
|
|
437
|
+
const ticket = await this.getWithSecrets(ticketId);
|
|
438
|
+
const at = new Date().toISOString();
|
|
439
|
+
return this.tickets(ticket.organization_id).updateWhere(ticketId, (query) => query.neq('status', enum_1.SupportTicketStatus.CLOSED), { status: enum_1.SupportTicketStatus.CLOSED, closed_at: at, updated_at: at });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
exports.SupportTicketsAdminResource = SupportTicketsAdminResource;
|
|
443
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/support/tickets/index.ts"],"names":[],"mappings":"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"}
|
package/dist/users/dto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateUserDto as CreateUserDtoType, GetUsersDto as GetUsersDtoType, UpdateUserDto as UpdateUserDtoType, UserIdDto as UserIdDtoType } from '@managemint-solutions/entities/users/dto';
|
|
1
|
+
import type { CreateUserDto as CreateUserDtoType, GetUsersDto as GetUsersDtoType, UpdateMyProfileDto as UpdateMyProfileDtoType, UpdateUserDto as UpdateUserDtoType, UserIdDto as UserIdDtoType } from '@managemint-solutions/entities/users/dto';
|
|
2
2
|
import { EmergencyContactRelationship, EmploymentType } from '@managemint-solutions/entities/users/enum';
|
|
3
3
|
export declare class UserIdDto implements UserIdDtoType {
|
|
4
4
|
readonly userId: string;
|
|
@@ -49,3 +49,29 @@ export declare class UpdateUserDto implements UpdateUserDtoType {
|
|
|
49
49
|
readonly emergency_contact_phone?: string | null;
|
|
50
50
|
readonly emergency_contact_email?: string | null;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* `PATCH /users/me` — what a member may change about themselves.
|
|
54
|
+
*
|
|
55
|
+
* Every field here also appears on UpdateUserDto with the same constraints; what makes
|
|
56
|
+
* this a separate class is what it *omits*. The api's ValidationPipe runs
|
|
57
|
+
* `whitelist + forbidNonWhitelisted`, so a self-service request carrying `job_title`,
|
|
58
|
+
* `manager_id`, `employment_type`, `start_date`, `end_date` or `profile_image` is
|
|
59
|
+
* rejected with a 400 rather than silently ignored — the narrowing is the guard.
|
|
60
|
+
*/
|
|
61
|
+
export declare class UpdateMyProfileDto implements UpdateMyProfileDtoType {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly surname: string;
|
|
64
|
+
readonly phone: string;
|
|
65
|
+
readonly country_code: string;
|
|
66
|
+
readonly birthday?: string | null;
|
|
67
|
+
readonly social_facebook?: string | null;
|
|
68
|
+
readonly social_linkedin?: string | null;
|
|
69
|
+
readonly social_twitter?: string | null;
|
|
70
|
+
readonly social_instagram?: string | null;
|
|
71
|
+
readonly social_github?: string | null;
|
|
72
|
+
readonly social_website?: string | null;
|
|
73
|
+
readonly emergency_contact_name?: string | null;
|
|
74
|
+
readonly emergency_contact_relationship?: EmergencyContactRelationship | null;
|
|
75
|
+
readonly emergency_contact_phone?: string | null;
|
|
76
|
+
readonly emergency_contact_email?: string | null;
|
|
77
|
+
}
|