ad2app-lib 1.17.0 → 1.20.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.
@@ -82,6 +82,12 @@ export class SchedulingInboxCommentDTO {
82
82
  /** Parent comment id on nested replies (AD2-1088). Absent on top-level comments. */
83
83
  parentId?: string;
84
84
  replies?: SchedulingInboxCommentDTO[];
85
+ /**
86
+ * True when this comment can be moderated (approve/reject/hold) — YouTube only
87
+ * per Zernio (OpenAPI v1.0.4:22489, `platform` enum = [youtube]). Absent for
88
+ * every other platform; never fabricated to false-as-if-known (082 US7).
89
+ */
90
+ canModerate?: boolean;
85
91
 
86
92
  constructor(data: SchedulingInboxCommentDTO) {
87
93
  this.id = data.id;
@@ -94,6 +100,7 @@ export class SchedulingInboxCommentDTO {
94
100
  this.isOwner = data.isOwner;
95
101
  this.parentId = data.parentId;
96
102
  this.replies = data.replies;
103
+ this.canModerate = data.canModerate;
97
104
  }
98
105
  }
99
106
 
@@ -128,3 +135,285 @@ export class SchedulingReplyToCommentDTO {
128
135
  this.commentId = data.commentId;
129
136
  }
130
137
  }
138
+
139
+ // ── 082 Inbox Parity — new inbox surfaces (US1) ───────────────────────────────
140
+ //
141
+ // Shapes below mirror the Zernio OpenAPI v1.0.4 inbox surface (line anchors in
142
+ // each comment); ad2app must NOT invent fields (AD2-1038). Availability is
143
+ // honest-or-absent: an optional field is absent when Zernio does not report it,
144
+ // never zero/false-backfilled.
145
+
146
+ // ── SchedulingInboxMessageDTO / SchedulingInboxMessagePageDTO ──────────────────
147
+
148
+ /**
149
+ * One message inside a DM thread.
150
+ * Returned (paginated) by GET /social/inbox/messages/:id/thread — backed by
151
+ * Zernio GET /v1/inbox/conversations/{id}/messages (OpenAPI v1.0.4:20845).
152
+ */
153
+ export class SchedulingInboxMessageDTO {
154
+ id: string;
155
+ conversationId: string;
156
+ authorId: string;
157
+ authorName: string;
158
+ text: string;
159
+ createdAt: string;
160
+ /** True when OUR connected account authored this message. */
161
+ isOwn: boolean;
162
+ mediaUrl?: string;
163
+
164
+ constructor(data: SchedulingInboxMessageDTO) {
165
+ this.id = data.id;
166
+ this.conversationId = data.conversationId;
167
+ this.authorId = data.authorId;
168
+ this.authorName = data.authorName;
169
+ this.text = data.text;
170
+ this.createdAt = data.createdAt;
171
+ this.isOwn = data.isOwn;
172
+ this.mediaUrl = data.mediaUrl;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * A cursor page of thread messages — kills the prior 100-message truncation.
178
+ * `nextCursor` absent = last page.
179
+ */
180
+ export class SchedulingInboxMessagePageDTO {
181
+ items: SchedulingInboxMessageDTO[];
182
+ nextCursor?: string;
183
+
184
+ constructor(data: SchedulingInboxMessagePageDTO) {
185
+ this.items = data.items;
186
+ this.nextCursor = data.nextCursor;
187
+ }
188
+ }
189
+
190
+ // ── SchedulingConversationReadDTO ─────────────────────────────────────────────
191
+
192
+ /**
193
+ * Mark-read acknowledgement — POST /social/inbox/messages/:id/read → Zernio
194
+ * POST /v1/inbox/conversations/{id}/read (OpenAPI v1.0.4:21692). `unreadCount`
195
+ * absent unless Zernio reports the post-read count.
196
+ */
197
+ export class SchedulingConversationReadDTO {
198
+ conversationId: string;
199
+ unreadCount?: number;
200
+
201
+ constructor(data: SchedulingConversationReadDTO) {
202
+ this.conversationId = data.conversationId;
203
+ this.unreadCount = data.unreadCount;
204
+ }
205
+ }
206
+
207
+ // ── SchedulingInboxMentionDTO / SchedulingReplyToMentionDTO ────────────────────
208
+
209
+ /**
210
+ * An inbound mention.
211
+ * Returned by GET /social/inbox/mentions → Zernio GET /v1/inbox/mentions
212
+ * (OpenAPI v1.0.4:23108). Source post fields absent when the platform omits them.
213
+ */
214
+ export class SchedulingInboxMentionDTO {
215
+ id: string;
216
+ platform: string;
217
+ accountId: string;
218
+ authorName: string;
219
+ authorAvatarUrl?: string;
220
+ text: string;
221
+ createdAt: string;
222
+ sourcePostId?: string;
223
+ sourcePostUrl?: string;
224
+
225
+ constructor(data: SchedulingInboxMentionDTO) {
226
+ this.id = data.id;
227
+ this.platform = data.platform;
228
+ this.accountId = data.accountId;
229
+ this.authorName = data.authorName;
230
+ this.authorAvatarUrl = data.authorAvatarUrl;
231
+ this.text = data.text;
232
+ this.createdAt = data.createdAt;
233
+ this.sourcePostId = data.sourcePostId;
234
+ this.sourcePostUrl = data.sourcePostUrl;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Input for replying to a mention. Zernio requires [accountId, mediaId, message]
240
+ * (OpenAPI v1.0.4:23207) — `mediaId` (the mention's target media) is mandatory,
241
+ * unlike a DM reply.
242
+ */
243
+ export class SchedulingReplyToMentionDTO {
244
+ accountId: string;
245
+ mediaId: string;
246
+ message: string;
247
+
248
+ constructor(data: SchedulingReplyToMentionDTO) {
249
+ this.accountId = data.accountId;
250
+ this.mediaId = data.mediaId;
251
+ this.message = data.message;
252
+ }
253
+ }
254
+
255
+ // ── SchedulingInboxReviewDTO / SchedulingReplyToReviewDTO ──────────────────────
256
+
257
+ /**
258
+ * A platform review (e.g. Google Business).
259
+ * Returned by GET /social/inbox/reviews → Zernio GET /v1/inbox/reviews
260
+ * (OpenAPI v1.0.4:23269). `replied` absent unless Zernio reports reply state.
261
+ */
262
+ export class SchedulingInboxReviewDTO {
263
+ id: string;
264
+ platform: string;
265
+ accountId: string;
266
+ authorName: string;
267
+ rating: number;
268
+ text: string;
269
+ createdAt: string;
270
+ replied?: boolean;
271
+
272
+ constructor(data: SchedulingInboxReviewDTO) {
273
+ this.id = data.id;
274
+ this.platform = data.platform;
275
+ this.accountId = data.accountId;
276
+ this.authorName = data.authorName;
277
+ this.rating = data.rating;
278
+ this.text = data.text;
279
+ this.createdAt = data.createdAt;
280
+ this.replied = data.replied;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Input for replying to a review — POST /social/inbox/reviews/:reviewId/reply →
286
+ * Zernio POST /v1/inbox/reviews/{reviewId}/reply (OpenAPI v1.0.4:23377).
287
+ */
288
+ export class SchedulingReplyToReviewDTO {
289
+ reviewId: string;
290
+ accountId: string;
291
+ text: string;
292
+
293
+ constructor(data: SchedulingReplyToReviewDTO) {
294
+ this.reviewId = data.reviewId;
295
+ this.accountId = data.accountId;
296
+ this.text = data.text;
297
+ }
298
+ }
299
+
300
+ // ── SchedulingCommentModerationDTO ────────────────────────────────────────────
301
+
302
+ /**
303
+ * The moderation decision on a comment. Closed set per Zernio
304
+ * (OpenAPI v1.0.4:22489): `published` approves, `rejected` removes,
305
+ * `heldForReview` returns it to the queue. YouTube only. `banAuthor` is valid
306
+ * ONLY with `rejected` (any other pairing is a 400 — enforced server-side).
307
+ */
308
+ export type SchedulingCommentModerationStatus = 'published' | 'rejected' | 'heldForReview';
309
+
310
+ export class SchedulingCommentModerationDTO {
311
+ moderationStatus: SchedulingCommentModerationStatus;
312
+ banAuthor?: boolean;
313
+
314
+ constructor(data: SchedulingCommentModerationDTO) {
315
+ this.moderationStatus = data.moderationStatus;
316
+ this.banAuthor = data.banAuthor;
317
+ }
318
+ }
319
+
320
+ // ── SchedulingCreateConversationDTO ───────────────────────────────────────────
321
+
322
+ /**
323
+ * Input for starting a new DM — POST /social/inbox/conversations → Zernio
324
+ * POST /v1/inbox/conversations (OpenAPI v1.0.4:20355). WhatsApp is excluded
325
+ * (it requires an approved template to open a thread; 082 non-goal).
326
+ */
327
+ export class SchedulingCreateConversationDTO {
328
+ accountId: string;
329
+ recipientId: string;
330
+ text: string;
331
+
332
+ constructor(data: SchedulingCreateConversationDTO) {
333
+ this.accountId = data.accountId;
334
+ this.recipientId = data.recipientId;
335
+ this.text = data.text;
336
+ }
337
+ }
338
+
339
+ // ── SchedulingInboxCapabilityDTO ──────────────────────────────────────────────
340
+
341
+ /**
342
+ * Per-connected-account inbox capability truth — which surfaces a platform
343
+ * actually supports. Drives honest showing/hiding (082 FR-4): a false/absent
344
+ * surface is HIDDEN, never rendered as a fabricated empty. Reconciled from
345
+ * platform-capabilities + Zernio /accounts/health.
346
+ */
347
+ export class SchedulingInboxCapabilityDTO {
348
+ platform: string;
349
+ dm: boolean;
350
+ comments: boolean;
351
+ mentions: boolean;
352
+ reviews: boolean;
353
+ /** Can INITIATE a DM (not just reply). */
354
+ dmInit: boolean;
355
+ /** Comment moderation queue (YouTube). */
356
+ moderation: boolean;
357
+
358
+ constructor(data: SchedulingInboxCapabilityDTO) {
359
+ this.platform = data.platform;
360
+ this.dm = data.dm;
361
+ this.comments = data.comments;
362
+ this.mentions = data.mentions;
363
+ this.reviews = data.reviews;
364
+ this.dmInit = data.dmInit;
365
+ this.moderation = data.moderation;
366
+ }
367
+ }
368
+
369
+ // ── SchedulingCrossPostCommentDTO ─────────────────────────────────────────────
370
+
371
+ /**
372
+ * A comment re-sourced from Zernio's cross-post inbox (GET /v1/inbox/comments),
373
+ * carrying its OWN post attribution so it surfaces even when the post is absent
374
+ * from our local SocialPostCache. The cache is enrichment ONLY (post snippet /
375
+ * thumbnail), never the source of truth (082 US3 / AD2-1110): a comment on an
376
+ * uncached post must still reach the moderator. Comment/engagement fields mirror
377
+ * SchedulingInboxCommentDTO; enrichment fields are honest-or-absent — filled from
378
+ * the cache when present, never zero/empty-backfilled.
379
+ */
380
+ export class SchedulingCrossPostCommentDTO {
381
+ // Comment identity + content (mirrors SchedulingInboxCommentDTO).
382
+ id: string;
383
+ authorName: string;
384
+ authorAvatarUrl?: string;
385
+ text: string;
386
+ createdAt: string;
387
+ likeCount?: number;
388
+ replyCount?: number;
389
+ isOwner?: boolean;
390
+ parentId?: string;
391
+ canModerate?: boolean;
392
+ replies?: SchedulingCrossPostCommentDTO[];
393
+ // Post attribution — from the cross-post payload, NOT the cache (the whole point of US3).
394
+ postId: string;
395
+ platform: string;
396
+ accountId: string;
397
+ // Enrichment — from SocialPostCache WHEN present, absent otherwise (never backfilled).
398
+ postContentSnippet?: string;
399
+ postThumbnailUrl?: string;
400
+
401
+ constructor(data: SchedulingCrossPostCommentDTO) {
402
+ this.id = data.id;
403
+ this.authorName = data.authorName;
404
+ this.authorAvatarUrl = data.authorAvatarUrl;
405
+ this.text = data.text;
406
+ this.createdAt = data.createdAt;
407
+ this.likeCount = data.likeCount;
408
+ this.replyCount = data.replyCount;
409
+ this.isOwner = data.isOwner;
410
+ this.parentId = data.parentId;
411
+ this.canModerate = data.canModerate;
412
+ this.replies = data.replies;
413
+ this.postId = data.postId;
414
+ this.platform = data.platform;
415
+ this.accountId = data.accountId;
416
+ this.postContentSnippet = data.postContentSnippet;
417
+ this.postThumbnailUrl = data.postThumbnailUrl;
418
+ }
419
+ }
@@ -102,3 +102,27 @@ test('SchedulingPostStatus gains PUBLISHING (AD2-1153 publish resilience)', () =
102
102
  const post = basePost({ status: SchedulingPostStatus.PUBLISHING });
103
103
  assert.equal(post.status, 'publishing');
104
104
  });
105
+
106
+ // AD2-1154 FR-2 — the backend's local PlatformStatusMap already has an
107
+ // 'unknown' member (a platform Zernio attempted but couldn't yet confirm
108
+ // either way — Paweł's Facebook incident); this pins the lib side so the
109
+ // backend's `platformStatuses: (...) as unknown as SchedulingPostDTO['platformStatuses']`
110
+ // cast in post-response.dto.ts can eventually be replaced by a direct import.
111
+ test("SchedulingPlatformStatusMap gains 'unknown' (AD2-1154 FR-2 honest reconcile state)", () => {
112
+ const post = basePost({ platformStatuses: { instagram: 'published', facebook: 'unknown' } });
113
+ assert.equal(post.platformStatuses.facebook, 'unknown');
114
+ });
115
+
116
+ // AD2-1154 FR-3a — the backend's local platform_retry_counts column already
117
+ // tracks per-platform attempts (a permanently-failing platform must never
118
+ // loop forever); this pins the lib side so the field can be exposed on the
119
+ // DTO without a cast once the backend swaps to it.
120
+ test('SchedulingPostDTO gains platformRetryCounts (AD2-1154 FR-3a terminal-retry tracking)', () => {
121
+ const post = basePost({ platformRetryCounts: { tiktok: 3 } });
122
+ assert.equal(post.platformRetryCounts.tiktok, 3);
123
+ });
124
+
125
+ test('SchedulingPostDTO platformRetryCounts is absent by default (backward compatible)', () => {
126
+ const post = basePost();
127
+ assert.equal(post.platformRetryCounts, undefined);
128
+ });
@@ -27,7 +27,12 @@ export enum SchedulingPostStatus {
27
27
 
28
28
  export type SchedulingPlatformStatusMap = Record<
29
29
  string,
30
- 'scheduled' | 'published' | 'failed'
30
+ // 'unknown': the publisher attempted this platform but its outcome isn't
31
+ // confirmable yet (e.g. Zernio 'pending'/'publishing') — distinct from
32
+ // 'scheduled' (not yet attempted at all). Must never render as a success
33
+ // or a failure (AD2-1154 FR-2, Paweł's incident: his Facebook post was
34
+ // neither confirmed nor failed).
35
+ 'scheduled' | 'published' | 'failed' | 'unknown'
31
36
  >;
32
37
 
33
38
  // ── SchedulingPlatformTargetDTO ───────────────────────────────────────────────
@@ -163,6 +168,12 @@ export class SchedulingPostDTO {
163
168
  postUrls?: Record<string, string>;
164
169
  /** Per-platform error messages for platforms that failed to publish. */
165
170
  platformErrors?: Record<string, string>;
171
+ /**
172
+ * Per-platform retry-attempt counts (AD2-1154 FR-3a). A platform that has
173
+ * used its 3rd attempt and is still failed is terminal — the retry
174
+ * affordance withdraws and the last real error is shown as permanent.
175
+ */
176
+ platformRetryCounts?: Record<string, number>;
166
177
  scheduledAt?: string;
167
178
  publishedAt?: string;
168
179
  status: SchedulingPostStatus;
@@ -196,6 +207,7 @@ export class SchedulingPostDTO {
196
207
  this.platformStatuses = data.platformStatuses;
197
208
  this.postUrls = data.postUrls;
198
209
  this.platformErrors = data.platformErrors;
210
+ this.platformRetryCounts = data.platformRetryCounts;
199
211
  this.scheduledAt = data.scheduledAt;
200
212
  this.publishedAt = data.publishedAt;
201
213
  this.status = data.status;