@teamscala/service-boot 0.23.361 → 0.23.362

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.361",
9
+ "version": "0.23.362",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -147,6 +147,27 @@ export function formatSpanMinutes(minutes: number): string {
147
147
  return `${hours >= 10 ? Math.round(hours) : Math.round(hours * 10) / 10} h`;
148
148
  }
149
149
 
150
+ /**
151
+ * The WhatsApp message id from a GOWA /send/message result. The canonical
152
+ * shape (from the spec the api-gowa adapter is generated from,
153
+ * SendResponse = { code, message, results: { message_id, status } }) is
154
+ * `results.message_id`; the legacy shape returned the bare id string as
155
+ * `results`. Delivery is only VERIFIED on a real id — `"true"`, a status
156
+ * string, or an empty body is not a message id and must not be recorded as
157
+ * delivered.
158
+ */
159
+ export function extractGowaMessageId(sendResult: unknown): string | null {
160
+ const results = (sendResult as { results?: unknown } | undefined)?.results;
161
+ if (typeof results === "string") {
162
+ return /^[A-Z0-9]{8,}$/i.test(results) ? results : null;
163
+ }
164
+ if (results && typeof results === "object") {
165
+ const id = (results as { message_id?: unknown }).message_id;
166
+ if (typeof id === "string" && id.length > 0) return id;
167
+ }
168
+ return null;
169
+ }
170
+
150
171
  /**
151
172
  * The escalation message — escalation-routing's three-field contract adapted
152
173
  * to an automated detector: what was OBSERVED (evidence), what it MEANS
@@ -12,6 +12,7 @@ import { describe, expect, test } from "bun:test";
12
12
  import {
13
13
  composeEscalationMessage,
14
14
  decideSweep,
15
+ extractGowaMessageId,
15
16
  type EscalationEpisode,
16
17
  type FaultAggregate,
17
18
  type FaultEscalationPolicy,
@@ -166,6 +167,26 @@ describe("decideSweep — resolution + re-arm", () => {
166
167
  });
167
168
  });
168
169
 
170
+ describe("extractGowaMessageId — delivery verification", () => {
171
+ test("the canonical GOWA SendResponse shape yields results.message_id", () => {
172
+ expect(extractGowaMessageId({ code: "SUCCESS", results: { message_id: "3EB089B9D6ADD58153C561", status: "sent" } })).toBe(
173
+ "3EB089B9D6ADD58153C561",
174
+ );
175
+ });
176
+
177
+ test("the legacy bare-string results shape yields the id", () => {
178
+ expect(extractGowaMessageId({ results: "3EB089B9D6ADD58153C561" })).toBe("3EB089B9D6ADD58153C561");
179
+ });
180
+
181
+ test("a 'true' ack is NOT a message id — delivery stays unverified", () => {
182
+ // A boolean-ish ack string must never be recorded as a delivered id:
183
+ // that would mark the episode delivered with an unfollowable handle.
184
+ expect(extractGowaMessageId({ results: "true" })).toBeNull();
185
+ expect(extractGowaMessageId({ code: "SUCCESS", message: "sent", results: { status: "sent" } })).toBeNull();
186
+ expect(extractGowaMessageId({ code: "SUCCESS" })).toBeNull();
187
+ });
188
+ });
189
+
169
190
  describe("composeEscalationMessage", () => {
170
191
  test("carries the three escalation-routing fields and stays phone-sized", () => {
171
192
  const message = composeEscalationMessage({
@@ -55,6 +55,7 @@ import {
55
55
  composeEscalationDescription,
56
56
  composeEscalationMessage,
57
57
  decideSweep,
58
+ extractGowaMessageId,
58
59
  type EscalationEpisode,
59
60
  type FaultAggregate,
60
61
  type KindThresholdOverride,
@@ -178,17 +179,6 @@ async function resolvePaDeviceId(paPhoneDigits: string): Promise<string | undefi
178
179
  return match?.id;
179
180
  }
180
181
 
181
- function extractMessageId(sendResult: unknown): string | null {
182
- const results = (sendResult as { results?: unknown } | undefined)?.results;
183
- const id =
184
- typeof results === "string"
185
- ? results
186
- : Array.isArray(results)
187
- ? ((results[0] as { id?: unknown } | undefined)?.id as string | undefined)
188
- : ((results as { id?: unknown } | undefined)?.id as string | undefined);
189
- return typeof id === "string" && id.length > 0 ? id : null;
190
- }
191
-
192
182
  async function deliverEscalation(
193
183
  kind: string,
194
184
  aggregate: FaultAggregate,
@@ -246,7 +236,7 @@ async function deliverEscalation(
246
236
 
247
237
  // 3. VERIFY delivery before recording it: the send must return a WhatsApp
248
238
  // message id. No id => throw, the claim stays undelivered, next sweep retries.
249
- const messageId = extractMessageId(sendResult);
239
+ const messageId = extractGowaMessageId(sendResult);
250
240
  if (!messageId) {
251
241
  throw new Error(
252
242
  `platform-fault-escalation: WhatsApp send for '${kind}' returned no message id — not recording delivery`,