Skip to content

Commit 7ca65ce

Browse files
committed
fix(plugin-approvals): approval notifications carry their text in body, the field messaging reads
Every notification the approvals service sent put its text in payload.message. The messaging service builds the delivered notification from payload.title and payload.body only (the inline fan-out, the outbox snapshot and the dispatcher alike), so GET /api/v1/notifications served each one as a title over an empty body. - The twelve call sites now send the text as body. The texts themselves are byte-identical; who is notified, and when, is unchanged. - notify() declares its payload (title, body, actionUrl, and a reminder's actions), so a call site that spells the text any other way does not compile. - No alias in service-messaging: one field, as EmitInput documents. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 68baad4 commit 7ca65ce

2 files changed

Lines changed: 48 additions & 14 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
Approval notifications reach their recipient with their text (#21847). The approvals service put each notification's text in `payload.message`. The messaging service builds the delivered notification from `payload.title` and `payload.body`, the fields its `EmitInput` documents, and no channel reads `message`. So `GET /api/v1/notifications` served every approval notification as a title over an empty `body`, and the inbox row's `body_md` was empty too. The lost texts were comments, request-info questions, send-back notes, reassignments, reminders, escalations, SLA breaches and out-of-office substitutions. Every one of them now travels in `payload.body`.
6+
7+
Clause-②: no
8+
9+
- The texts are unchanged. Only the field they travel in moved. Who is notified, and when, is unchanged.
10+
- Approval notifications no longer carry `payload.message`. Nothing in the platform or the console read it. A tenant-authored `sys_notification_template` for an `approval.*` topic that wrote `{{ message }}` should write `{{ body }}`.
11+
- The service's notify helper now declares its payload: `title`, `body`, `actionUrl`, and a reminder's `actions`. A call site that spells the text any other way no longer compiles.
12+
- `@objectstack/service-messaging` is unchanged. There is one field, as documented, and no alias for the old one.

‎packages/plugins/plugin-approvals/src/approval-service.ts‎

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,28 @@ export interface ApprovalMessagingSurface {
280280
}): Promise<unknown>;
281281
}
282282

283+
/**
284+
* What an approval notification hands to the messaging service: the
285+
* `EmitInput` payload fields the delivered notification is built from, plus a
286+
* reminder's one-tap decision links.
287+
*
288+
* `body` is the text the recipient reads under the title. Messaging builds the
289+
* inbox row's `body_md`, and each channel's fallback rendering, from
290+
* `payload.body` and no other field, so the text is declared here rather than
291+
* left to a free-form bag: a call site that spells it any other way does not
292+
* compile. Spelled `message`, it once reached every recipient as an empty body.
293+
*/
294+
type ApprovalNotificationPayload = {
295+
/** The headline. */
296+
readonly title: string;
297+
/** The text under the headline. */
298+
readonly body: string;
299+
/** Where opening it lands; `notify()` narrows the approvals inbox to the request. */
300+
readonly actionUrl: string;
301+
/** Approve / Reject links on a reminder to a concrete approver (ADR-0043). */
302+
readonly actions?: ReadonlyArray<{ readonly label: string; readonly url: string }>;
303+
};
304+
283305
/** Minimum time between submitter reminders on one request. */
284306
export const REMIND_COOLDOWN_MS = 4 * 60 * 60 * 1000;
285307

@@ -1373,7 +1395,7 @@ export class ApprovalService implements IApprovalService {
13731395
private async notify(input: {
13741396
topic: string;
13751397
audience: string[];
1376-
payload?: Record<string, unknown>;
1398+
payload: ApprovalNotificationPayload;
13771399
dedupKey?: string;
13781400
source?: { object: string; id: string };
13791401
actorContext?: ExecutionContext;
@@ -1388,7 +1410,7 @@ export class ApprovalService implements IApprovalService {
13881410
// the console inbox to auto-open the drawer.
13891411
let payload = event.payload;
13901412
if (
1391-
payload?.actionUrl === '/system/approvals'
1413+
payload.actionUrl === '/system/approvals'
13921414
&& event.source?.object === 'sys_approval_request'
13931415
&& event.source.id
13941416
) {
@@ -3073,7 +3095,7 @@ export class ApprovalService implements IApprovalService {
30733095
dedupKey: `approval-ooo-${id}-${sub.to}`,
30743096
payload: {
30753097
title: 'Approval routed to you (out-of-office cover)',
3076-
message: `You are covering an approval on ${input.object}/${input.recordId} while ${sub.from} is out of office.`,
3098+
body: `You are covering an approval on ${input.object}/${input.recordId} while ${sub.from} is out of office.`,
30773099
actionUrl: '/system/approvals',
30783100
},
30793101
});
@@ -3084,7 +3106,7 @@ export class ApprovalService implements IApprovalService {
30843106
dedupKey: `approval-ooo-skip-${id}-${sub.from}`,
30853107
payload: {
30863108
title: 'Approval routed to your delegate',
3087-
message: `An approval on ${input.object}/${input.recordId} was routed to ${sub.to} while you are out of office.`,
3109+
body: `An approval on ${input.object}/${input.recordId} was routed to ${sub.to} while you are out of office.`,
30883110
actionUrl: '/system/approvals',
30893111
},
30903112
});
@@ -4206,7 +4228,7 @@ export class ApprovalService implements IApprovalService {
42064228
source: { object: 'sys_approval_request', id: requestId },
42074229
payload: {
42084230
title: 'Approval auto-rejected',
4209-
message: `Your ${raw.object_name}/${raw.record_id} exceeded the revision limit (${maxRevisions}) and was rejected.`,
4231+
body: `Your ${raw.object_name}/${raw.record_id} exceeded the revision limit (${maxRevisions}) and was rejected.`,
42104232
actionUrl: '/system/approvals',
42114233
},
42124234
});
@@ -4249,7 +4271,7 @@ export class ApprovalService implements IApprovalService {
42494271
source: { object: 'sys_approval_request', id: requestId },
42504272
payload: {
42514273
title: 'Sent back for revision',
4252-
message: input.comment?.trim() || `Your ${raw.object_name}/${raw.record_id} needs rework before it can be approved.`,
4274+
body: input.comment?.trim() || `Your ${raw.object_name}/${raw.record_id} needs rework before it can be approved.`,
42534275
actionUrl: '/system/approvals',
42544276
},
42554277
});
@@ -4509,7 +4531,7 @@ export class ApprovalService implements IApprovalService {
45094531
dedupKey: `approval-reassign-${requestId}-${to}`,
45104532
payload: {
45114533
title: 'Approval handed to you',
4512-
message: `You are now an approver on ${raw.object_name}/${raw.record_id}.`,
4534+
body: `You are now an approver on ${raw.object_name}/${raw.record_id}.`,
45134535
actionUrl: '/system/approvals',
45144536
},
45154537
});
@@ -4568,7 +4590,7 @@ export class ApprovalService implements IApprovalService {
45684590
dedupKey: `approval-remind-${requestId}-${nowIso}-${approver}`,
45694591
payload: {
45704592
title: 'Approval reminder',
4571-
message: `A decision on ${raw.object_name}/${raw.record_id} is still waiting on you.`,
4593+
body: `A decision on ${raw.object_name}/${raw.record_id} is still waiting on you.`,
45724594
actionUrl: '/system/approvals',
45734595
actions: [
45744596
{ label: 'Approve', url: this.actionLinkUrl(tokens.approve) },
@@ -4591,7 +4613,7 @@ export class ApprovalService implements IApprovalService {
45914613
dedupKey: `approval-remind-${requestId}-${nowIso}`,
45924614
payload: {
45934615
title: 'Approval reminder',
4594-
message: `A decision on ${raw.object_name}/${raw.record_id} is still waiting on you.`,
4616+
body: `A decision on ${raw.object_name}/${raw.record_id} is still waiting on you.`,
45954617
actionUrl: '/system/approvals',
45964618
},
45974619
});
@@ -4785,7 +4807,7 @@ export class ApprovalService implements IApprovalService {
47854807
source: { object: 'sys_approval_request', id: requestId },
47864808
payload: {
47874809
title: 'More information requested',
4788-
message: input.comment.trim(),
4810+
body: input.comment.trim(),
47894811
actionUrl: '/system/approvals',
47904812
},
47914813
});
@@ -4832,7 +4854,7 @@ export class ApprovalService implements IApprovalService {
48324854
source: { object: 'sys_approval_request', id: requestId },
48334855
payload: {
48344856
title: 'New comment on an approval',
4835-
message: input.comment.trim(),
4857+
body: input.comment.trim(),
48364858
actionUrl: '/system/approvals',
48374859
},
48384860
});
@@ -5877,7 +5899,7 @@ export class ApprovalService implements IApprovalService {
58775899
source: { object: 'sys_approval_request', id: raw.id },
58785900
payload: {
58795901
title: 'Approval escalated to you',
5880-
message: `An overdue approval on ${raw.object_name}/${raw.record_id} was escalated to you.`,
5902+
body: `An overdue approval on ${raw.object_name}/${raw.record_id} was escalated to you.`,
58815903
actionUrl: '/system/approvals',
58825904
},
58835905
});
@@ -5895,7 +5917,7 @@ export class ApprovalService implements IApprovalService {
58955917
source: { object: 'sys_approval_request', id: raw.id },
58965918
payload: {
58975919
title: 'Approval SLA breached',
5898-
message: `A decision on ${raw.object_name}/${raw.record_id} is overdue.`,
5920+
body: `A decision on ${raw.object_name}/${raw.record_id} is overdue.`,
58995921
actionUrl: '/system/approvals',
59005922
},
59015923
});
@@ -5908,7 +5930,7 @@ export class ApprovalService implements IApprovalService {
59085930
source: { object: 'sys_approval_request', id: raw.id },
59095931
payload: {
59105932
title: 'Your approval request breached its SLA',
5911-
message: `${raw.object_name}/${raw.record_id}: escalation action '${action}' was taken.`,
5933+
body: `${raw.object_name}/${raw.record_id}: escalation action '${action}' was taken.`,
59125934
actionUrl: '/system/approvals',
59135935
},
59145936
});

0 commit comments

Comments
 (0)