@grapadigital/shared-schemas 1.0.221 → 1.0.223

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/index.d.ts CHANGED
@@ -37,3 +37,4 @@ export * from './schemas/job.schema';
37
37
  export * from './schemas/snapshot.schema';
38
38
  export * from './schemas/lead.schema';
39
39
  export * from './utils/normalize-content-link.util';
40
+ export * from './utils/merge-metrics-breakdown.util';
package/dist/index.js CHANGED
@@ -58,3 +58,4 @@ __exportStar(require("./schemas/job.schema"), exports);
58
58
  __exportStar(require("./schemas/snapshot.schema"), exports);
59
59
  __exportStar(require("./schemas/lead.schema"), exports);
60
60
  __exportStar(require("./utils/normalize-content-link.util"), exports);
61
+ __exportStar(require("./utils/merge-metrics-breakdown.util"), exports);
@@ -113,6 +113,22 @@ export declare class Metrics {
113
113
  cpmi?: number | null;
114
114
  cpi?: number | null;
115
115
  cpe?: number | null;
116
+ /**
117
+ * Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
118
+ * Content.metrics soma o autor e todos os coautores. Ver GD-703.
119
+ */
120
+ potentialReach?: number | null;
121
+ /** Tx. de Engaj. por Alcance: engagementCount ÷ reachCount (fração). Ver GD-703. */
122
+ reachEngagementRate?: number | null;
123
+ /** Tx. de Engaj. por Seguidores: engagementCount ÷ potentialReach (fração). Ver GD-703. */
124
+ followersEngagementRate?: number | null;
125
+ /** Nome do Client.tiers pelo potentialReach do post; null sem Tiers ou fora das faixas. Ver GD-703. */
126
+ tier?: string | null;
127
+ /**
128
+ * followersEngagementRate ≥ meta viralReachEngagementRate do Tier (nome da
129
+ * meta herdado da GD-687); null sem Tier ou sem a meta. Ver GD-703.
130
+ */
131
+ isViral?: boolean | null;
116
132
  }
117
133
  export declare const MetricsSchema: MongooseSchema<Metrics, import("mongoose").Model<Metrics, any, any, any, import("mongoose").Document<unknown, any, Metrics, any, {}> & Metrics & {
118
134
  _id: Types.ObjectId;
@@ -123,6 +139,85 @@ export declare const MetricsSchema: MongooseSchema<Metrics, import("mongoose").M
123
139
  } & {
124
140
  __v: number;
125
141
  }>;
142
+ /** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
143
+ export declare class ContentCollab {
144
+ profile: Types.ObjectId | Profile;
145
+ followersAtPost?: number | null;
146
+ }
147
+ export declare const ContentCollabSchema: MongooseSchema<ContentCollab, import("mongoose").Model<ContentCollab, any, any, any, import("mongoose").Document<unknown, any, ContentCollab, any, {}> & ContentCollab & {
148
+ _id: Types.ObjectId;
149
+ } & {
150
+ __v: number;
151
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentCollab, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentCollab>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentCollab> & {
152
+ _id: Types.ObjectId;
153
+ } & {
154
+ __v: number;
155
+ }>;
156
+ /** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
157
+ export declare const CONTENT_BASE_METRIC_KEYS: readonly ["impressionCount", "reachCount", "likeCount", "commentCount", "savedCount", "sharesCount", "storyCount", "tapBackCount", "clickCount", "replyCount", "tapForwardCount", "swipeForwardCount", "exitCount", "favoriteCount", "dislikeCount", "emv"];
158
+ export type ContentBaseMetricKey = (typeof CONTENT_BASE_METRIC_KEYS)[number];
159
+ export declare class ContentBaseMetrics {
160
+ impressionCount?: number | null;
161
+ reachCount?: number | null;
162
+ likeCount?: number | null;
163
+ commentCount?: number | null;
164
+ savedCount?: number | null;
165
+ sharesCount?: number | null;
166
+ storyCount?: number | null;
167
+ tapBackCount?: number | null;
168
+ clickCount?: number | null;
169
+ replyCount?: number | null;
170
+ tapForwardCount?: number | null;
171
+ swipeForwardCount?: number | null;
172
+ exitCount?: number | null;
173
+ favoriteCount?: number | null;
174
+ dislikeCount?: number | null;
175
+ emv?: number | null;
176
+ }
177
+ export declare const ContentBaseMetricsSchema: MongooseSchema<ContentBaseMetrics, import("mongoose").Model<ContentBaseMetrics, any, any, any, import("mongoose").Document<unknown, any, ContentBaseMetrics, any, {}> & ContentBaseMetrics & {
178
+ _id: Types.ObjectId;
179
+ } & {
180
+ __v: number;
181
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentBaseMetrics, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentBaseMetrics>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentBaseMetrics> & {
182
+ _id: Types.ObjectId;
183
+ } & {
184
+ __v: number;
185
+ }>;
186
+ /** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
187
+ export declare class ContentMetricsBreakdown {
188
+ organic: ContentBaseMetrics;
189
+ boosted: ContentBaseMetrics;
190
+ }
191
+ export declare const ContentMetricsBreakdownSchema: MongooseSchema<ContentMetricsBreakdown, import("mongoose").Model<ContentMetricsBreakdown, any, any, any, import("mongoose").Document<unknown, any, ContentMetricsBreakdown, any, {}> & ContentMetricsBreakdown & {
192
+ _id: Types.ObjectId;
193
+ } & {
194
+ __v: number;
195
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentMetricsBreakdown, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentMetricsBreakdown>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentMetricsBreakdown> & {
196
+ _id: Types.ObjectId;
197
+ } & {
198
+ __v: number;
199
+ }>;
200
+ /**
201
+ * Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
202
+ * sempre Content.metrics.impressionCount. Ver GD-707.
203
+ */
204
+ export declare class ContentVideo {
205
+ /** Duração em segundos. */
206
+ duration?: number | null;
207
+ /** Tempo total assistido, somando todas as visualizações, em segundos. */
208
+ watchTime?: number | null;
209
+ /** (watchTime ÷ duration) ÷ impressionCount, sem teto. */
210
+ vcr?: number | null;
211
+ }
212
+ export declare const ContentVideoSchema: MongooseSchema<ContentVideo, import("mongoose").Model<ContentVideo, any, any, any, import("mongoose").Document<unknown, any, ContentVideo, any, {}> & ContentVideo & {
213
+ _id: Types.ObjectId;
214
+ } & {
215
+ __v: number;
216
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentVideo, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentVideo>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentVideo> & {
217
+ _id: Types.ObjectId;
218
+ } & {
219
+ __v: number;
220
+ }>;
126
221
  export declare class ContentMetricsHistoryEntry {
127
222
  metrics: Metrics;
128
223
  recordedAt: Date;
@@ -195,8 +290,17 @@ export declare class Content {
195
290
  isCollab: boolean;
196
291
  isBoosted: boolean;
197
292
  hasAffiliateLink: boolean;
198
- collabs?: Types.ObjectId[] | Profile[];
293
+ /** Coautores do post; isCollab = collabs.length > 0. Ver GD-703. */
294
+ collabs?: ContentCollab[];
295
+ /** Métricas do post (sem métrica por pessoa): derivadas com o potentialReach somado. Ver GD-703. */
199
296
  metrics: Metrics;
297
+ metricsBreakdown?: ContentMetricsBreakdown | null;
298
+ /**
299
+ * Caminhos das partes digitadas à mão (ex.: organic.reachCount,
300
+ * video.watchTime): a captura não sobrescreve. Ver GD-707.
301
+ */
302
+ manualMetrics?: string[];
303
+ video?: ContentVideo | null;
200
304
  estimates: Estimates;
201
305
  googleSheetRow?: string;
202
306
  createdAt?: Date;
@@ -206,7 +310,9 @@ export declare class Content {
206
310
  sentimentAnalysis?: SentimentAnalysis | null;
207
311
  thumbnailUrl?: string;
208
312
  mediaUrl?: string;
313
+ /** @deprecated Obsoleto desde a GD-707: a edição vale por parte (manualMetrics). */
209
314
  isEdited?: boolean;
315
+ /** @deprecated Obsoleto desde a GD-707. */
210
316
  editedAt?: Date;
211
317
  followersAtPost?: number | null;
212
318
  history?: ContentMetricsHistoryEntry[];
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = exports.CONTENT_METRICS_SOURCE_VALUES = exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = exports.CONTENT_REPORTABLE_STATUS_VALUES = exports.CONTENT_STATUS_VALUES = exports.EstimatesSchema = exports.Estimates = exports.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
12
+ exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = exports.CONTENT_METRICS_SOURCE_VALUES = exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = exports.CONTENT_REPORTABLE_STATUS_VALUES = exports.CONTENT_STATUS_VALUES = exports.EstimatesSchema = exports.Estimates = exports.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.ContentVideoSchema = exports.ContentVideo = exports.ContentMetricsBreakdownSchema = exports.ContentMetricsBreakdown = exports.ContentBaseMetricsSchema = exports.ContentBaseMetrics = exports.CONTENT_BASE_METRIC_KEYS = exports.ContentCollabSchema = exports.ContentCollab = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
15
  const normalize_content_link_util_1 = require("../utils/normalize-content-link.util");
@@ -207,11 +207,176 @@ __decorate([
207
207
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
208
208
  __metadata("design:type", Object)
209
209
  ], Metrics.prototype, "cpe", void 0);
210
+ __decorate([
211
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
212
+ __metadata("design:type", Object)
213
+ ], Metrics.prototype, "potentialReach", void 0);
214
+ __decorate([
215
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
216
+ __metadata("design:type", Object)
217
+ ], Metrics.prototype, "reachEngagementRate", void 0);
218
+ __decorate([
219
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
220
+ __metadata("design:type", Object)
221
+ ], Metrics.prototype, "followersEngagementRate", void 0);
222
+ __decorate([
223
+ (0, mongoose_1.Prop)({ type: String, required: false, default: null }),
224
+ __metadata("design:type", Object)
225
+ ], Metrics.prototype, "tier", void 0);
226
+ __decorate([
227
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: null }),
228
+ __metadata("design:type", Object)
229
+ ], Metrics.prototype, "isViral", void 0);
210
230
  Metrics = __decorate([
211
231
  (0, mongoose_1.Schema)({ _id: false })
212
232
  ], Metrics);
213
233
  exports.Metrics = Metrics;
214
234
  exports.MetricsSchema = mongoose_1.SchemaFactory.createForClass(Metrics);
235
+ /** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
236
+ let ContentCollab = class ContentCollab {
237
+ };
238
+ __decorate([
239
+ (0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: true }),
240
+ __metadata("design:type", Object)
241
+ ], ContentCollab.prototype, "profile", void 0);
242
+ __decorate([
243
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
244
+ __metadata("design:type", Object)
245
+ ], ContentCollab.prototype, "followersAtPost", void 0);
246
+ ContentCollab = __decorate([
247
+ (0, mongoose_1.Schema)({ _id: false })
248
+ ], ContentCollab);
249
+ exports.ContentCollab = ContentCollab;
250
+ exports.ContentCollabSchema = mongoose_1.SchemaFactory.createForClass(ContentCollab);
251
+ /** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
252
+ exports.CONTENT_BASE_METRIC_KEYS = [
253
+ 'impressionCount',
254
+ 'reachCount',
255
+ 'likeCount',
256
+ 'commentCount',
257
+ 'savedCount',
258
+ 'sharesCount',
259
+ 'storyCount',
260
+ 'tapBackCount',
261
+ 'clickCount',
262
+ 'replyCount',
263
+ 'tapForwardCount',
264
+ 'swipeForwardCount',
265
+ 'exitCount',
266
+ 'favoriteCount',
267
+ 'dislikeCount',
268
+ 'emv',
269
+ ];
270
+ let ContentBaseMetrics = class ContentBaseMetrics {
271
+ };
272
+ __decorate([
273
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
274
+ __metadata("design:type", Object)
275
+ ], ContentBaseMetrics.prototype, "impressionCount", void 0);
276
+ __decorate([
277
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
278
+ __metadata("design:type", Object)
279
+ ], ContentBaseMetrics.prototype, "reachCount", void 0);
280
+ __decorate([
281
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
282
+ __metadata("design:type", Object)
283
+ ], ContentBaseMetrics.prototype, "likeCount", void 0);
284
+ __decorate([
285
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
286
+ __metadata("design:type", Object)
287
+ ], ContentBaseMetrics.prototype, "commentCount", void 0);
288
+ __decorate([
289
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
290
+ __metadata("design:type", Object)
291
+ ], ContentBaseMetrics.prototype, "savedCount", void 0);
292
+ __decorate([
293
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
294
+ __metadata("design:type", Object)
295
+ ], ContentBaseMetrics.prototype, "sharesCount", void 0);
296
+ __decorate([
297
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
298
+ __metadata("design:type", Object)
299
+ ], ContentBaseMetrics.prototype, "storyCount", void 0);
300
+ __decorate([
301
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
302
+ __metadata("design:type", Object)
303
+ ], ContentBaseMetrics.prototype, "tapBackCount", void 0);
304
+ __decorate([
305
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
306
+ __metadata("design:type", Object)
307
+ ], ContentBaseMetrics.prototype, "clickCount", void 0);
308
+ __decorate([
309
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
310
+ __metadata("design:type", Object)
311
+ ], ContentBaseMetrics.prototype, "replyCount", void 0);
312
+ __decorate([
313
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
314
+ __metadata("design:type", Object)
315
+ ], ContentBaseMetrics.prototype, "tapForwardCount", void 0);
316
+ __decorate([
317
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
318
+ __metadata("design:type", Object)
319
+ ], ContentBaseMetrics.prototype, "swipeForwardCount", void 0);
320
+ __decorate([
321
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
322
+ __metadata("design:type", Object)
323
+ ], ContentBaseMetrics.prototype, "exitCount", void 0);
324
+ __decorate([
325
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
326
+ __metadata("design:type", Object)
327
+ ], ContentBaseMetrics.prototype, "favoriteCount", void 0);
328
+ __decorate([
329
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
330
+ __metadata("design:type", Object)
331
+ ], ContentBaseMetrics.prototype, "dislikeCount", void 0);
332
+ __decorate([
333
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
334
+ __metadata("design:type", Object)
335
+ ], ContentBaseMetrics.prototype, "emv", void 0);
336
+ ContentBaseMetrics = __decorate([
337
+ (0, mongoose_1.Schema)({ _id: false })
338
+ ], ContentBaseMetrics);
339
+ exports.ContentBaseMetrics = ContentBaseMetrics;
340
+ exports.ContentBaseMetricsSchema = mongoose_1.SchemaFactory.createForClass(ContentBaseMetrics);
341
+ /** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
342
+ let ContentMetricsBreakdown = class ContentMetricsBreakdown {
343
+ };
344
+ __decorate([
345
+ (0, mongoose_1.Prop)({ type: exports.ContentBaseMetricsSchema, required: false, default: () => ({}) }),
346
+ __metadata("design:type", ContentBaseMetrics)
347
+ ], ContentMetricsBreakdown.prototype, "organic", void 0);
348
+ __decorate([
349
+ (0, mongoose_1.Prop)({ type: exports.ContentBaseMetricsSchema, required: false, default: () => ({}) }),
350
+ __metadata("design:type", ContentBaseMetrics)
351
+ ], ContentMetricsBreakdown.prototype, "boosted", void 0);
352
+ ContentMetricsBreakdown = __decorate([
353
+ (0, mongoose_1.Schema)({ _id: false })
354
+ ], ContentMetricsBreakdown);
355
+ exports.ContentMetricsBreakdown = ContentMetricsBreakdown;
356
+ exports.ContentMetricsBreakdownSchema = mongoose_1.SchemaFactory.createForClass(ContentMetricsBreakdown);
357
+ /**
358
+ * Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
359
+ * sempre Content.metrics.impressionCount. Ver GD-707.
360
+ */
361
+ let ContentVideo = class ContentVideo {
362
+ };
363
+ __decorate([
364
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
365
+ __metadata("design:type", Object)
366
+ ], ContentVideo.prototype, "duration", void 0);
367
+ __decorate([
368
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
369
+ __metadata("design:type", Object)
370
+ ], ContentVideo.prototype, "watchTime", void 0);
371
+ __decorate([
372
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
373
+ __metadata("design:type", Object)
374
+ ], ContentVideo.prototype, "vcr", void 0);
375
+ ContentVideo = __decorate([
376
+ (0, mongoose_1.Schema)({ _id: false })
377
+ ], ContentVideo);
378
+ exports.ContentVideo = ContentVideo;
379
+ exports.ContentVideoSchema = mongoose_1.SchemaFactory.createForClass(ContentVideo);
215
380
  let ContentMetricsHistoryEntry = class ContentMetricsHistoryEntry {
216
381
  };
217
382
  __decorate([
@@ -391,15 +556,25 @@ __decorate([
391
556
  __metadata("design:type", Boolean)
392
557
  ], Content.prototype, "hasAffiliateLink", void 0);
393
558
  __decorate([
394
- (0, mongoose_1.Prop)([
395
- { type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: false },
396
- ]),
559
+ (0, mongoose_1.Prop)({ type: [exports.ContentCollabSchema], required: false, default: [] }),
397
560
  __metadata("design:type", Array)
398
561
  ], Content.prototype, "collabs", void 0);
399
562
  __decorate([
400
563
  (0, mongoose_1.Prop)({ type: exports.MetricsSchema, required: true }),
401
564
  __metadata("design:type", Metrics)
402
565
  ], Content.prototype, "metrics", void 0);
566
+ __decorate([
567
+ (0, mongoose_1.Prop)({ type: exports.ContentMetricsBreakdownSchema, required: false, default: null }),
568
+ __metadata("design:type", Object)
569
+ ], Content.prototype, "metricsBreakdown", void 0);
570
+ __decorate([
571
+ (0, mongoose_1.Prop)({ type: [String], required: false, default: [] }),
572
+ __metadata("design:type", Array)
573
+ ], Content.prototype, "manualMetrics", void 0);
574
+ __decorate([
575
+ (0, mongoose_1.Prop)({ type: exports.ContentVideoSchema, required: false, default: null }),
576
+ __metadata("design:type", Object)
577
+ ], Content.prototype, "video", void 0);
403
578
  __decorate([
404
579
  (0, mongoose_1.Prop)({ type: exports.MetricsSchema, required: true }),
405
580
  __metadata("design:type", Estimates)
@@ -500,6 +675,8 @@ exports.ContentSchema.index({ createdAt: -1, status: 1 });
500
675
  // Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
501
676
  exports.ContentSchema.index({ primetagId: 1 });
502
677
  exports.ContentSchema.index({ linkKey: 1 });
678
+ // Visão por Influenciador (GD-703): posts em que o Perfil é coautor.
679
+ exports.ContentSchema.index({ 'collabs.profile': 1 });
503
680
  // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
504
681
  // primetagId) ficam fora do índice.
505
682
  exports.ContentSchema.index({ action: 1, primetagId: 1 }, { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } });
@@ -0,0 +1,12 @@
1
+ import { ContentMetricsBreakdown, Metrics } from '../schemas/content.schema';
2
+ /**
3
+ * Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
4
+ *
5
+ * - metrics.<chave> = organic + boosted (null só se as duas forem null);
6
+ * - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
7
+ * comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
8
+ * - engagementRate = engagementCount ÷ impressionCount.
9
+ *
10
+ * Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
11
+ */
12
+ export declare const mergeMetricsBreakdown: (breakdown: ContentMetricsBreakdown, primetagEngagementCount: number | null | undefined, manualMetrics?: string[]) => Partial<Metrics>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeMetricsBreakdown = void 0;
4
+ const content_schema_1 = require("../schemas/content.schema");
5
+ // Partes que, se digitadas à mão, fazem o engajamento virar a soma.
6
+ const ENGAGEMENT_KEYS = ['likeCount', 'commentCount', 'sharesCount', 'savedCount'];
7
+ const sumParts = (organic, boosted) => organic == null && boosted == null ? null : (organic ?? 0) + (boosted ?? 0);
8
+ /**
9
+ * Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
10
+ *
11
+ * - metrics.<chave> = organic + boosted (null só se as duas forem null);
12
+ * - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
13
+ * comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
14
+ * - engagementRate = engagementCount ÷ impressionCount.
15
+ *
16
+ * Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
17
+ */
18
+ const mergeMetricsBreakdown = (breakdown, primetagEngagementCount, manualMetrics = []) => {
19
+ const organic = breakdown.organic ?? {};
20
+ const boosted = breakdown.boosted ?? {};
21
+ const totals = {};
22
+ for (const key of content_schema_1.CONTENT_BASE_METRIC_KEYS) {
23
+ totals[key] = sumParts(organic[key], boosted[key]);
24
+ }
25
+ const hasManualEngagement = ENGAGEMENT_KEYS.some((key) => manualMetrics.includes(`organic.${key}`) || manualMetrics.includes(`boosted.${key}`));
26
+ const engagementCount = hasManualEngagement
27
+ ? ENGAGEMENT_KEYS.reduce((sum, key) => sum + (totals[key] ?? 0), 0)
28
+ : primetagEngagementCount ?? null;
29
+ const impressionCount = totals.impressionCount ?? 0;
30
+ return {
31
+ ...totals,
32
+ engagementCount,
33
+ engagementRate: engagementCount != null && impressionCount > 0 ? engagementCount / impressionCount : null,
34
+ };
35
+ };
36
+ exports.mergeMetricsBreakdown = mergeMetricsBreakdown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grapadigital/shared-schemas",
3
- "version": "1.0.221",
3
+ "version": "1.0.223",
4
4
  "description": "Shared Mongoose Schemas",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -37,3 +37,4 @@ export * from './schemas/job.schema';
37
37
  export * from './schemas/snapshot.schema';
38
38
  export * from './schemas/lead.schema';
39
39
  export * from './utils/normalize-content-link.util';
40
+ export * from './utils/merge-metrics-breakdown.util';
@@ -157,10 +157,155 @@ export class Metrics {
157
157
 
158
158
  @Prop({ type: Number, required: false, default: null })
159
159
  cpe?: number | null;
160
+
161
+ /**
162
+ * Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
163
+ * Content.metrics soma o autor e todos os coautores. Ver GD-703.
164
+ */
165
+ @Prop({ type: Number, required: false, default: null })
166
+ potentialReach?: number | null;
167
+
168
+ /** Tx. de Engaj. por Alcance: engagementCount ÷ reachCount (fração). Ver GD-703. */
169
+ @Prop({ type: Number, required: false, default: null })
170
+ reachEngagementRate?: number | null;
171
+
172
+ /** Tx. de Engaj. por Seguidores: engagementCount ÷ potentialReach (fração). Ver GD-703. */
173
+ @Prop({ type: Number, required: false, default: null })
174
+ followersEngagementRate?: number | null;
175
+
176
+ /** Nome do Client.tiers pelo potentialReach do post; null sem Tiers ou fora das faixas. Ver GD-703. */
177
+ @Prop({ type: String, required: false, default: null })
178
+ tier?: string | null;
179
+
180
+ /**
181
+ * followersEngagementRate ≥ meta viralReachEngagementRate do Tier (nome da
182
+ * meta herdado da GD-687); null sem Tier ou sem a meta. Ver GD-703.
183
+ */
184
+ @Prop({ type: Boolean, required: false, default: null })
185
+ isViral?: boolean | null;
160
186
  }
161
187
 
162
188
  export const MetricsSchema = SchemaFactory.createForClass(Metrics);
163
189
 
190
+ /** Coautor do post (Colab), com os seguidores dele no momento do post. Ver GD-703. */
191
+ @Schema({ _id: false })
192
+ export class ContentCollab {
193
+ @Prop({ type: MongooseSchema.Types.ObjectId, ref: 'Profile', required: true })
194
+ profile: Types.ObjectId | Profile;
195
+
196
+ @Prop({ type: Number, required: false, default: null })
197
+ followersAtPost?: number | null;
198
+ }
199
+
200
+ export const ContentCollabSchema = SchemaFactory.createForClass(ContentCollab);
201
+
202
+ /** Chaves brutas de Metrics que têm parte Orgânica e Impulsionada. Ver GD-707. */
203
+ export const CONTENT_BASE_METRIC_KEYS = [
204
+ 'impressionCount',
205
+ 'reachCount',
206
+ 'likeCount',
207
+ 'commentCount',
208
+ 'savedCount',
209
+ 'sharesCount',
210
+ 'storyCount',
211
+ 'tapBackCount',
212
+ 'clickCount',
213
+ 'replyCount',
214
+ 'tapForwardCount',
215
+ 'swipeForwardCount',
216
+ 'exitCount',
217
+ 'favoriteCount',
218
+ 'dislikeCount',
219
+ 'emv',
220
+ ] as const;
221
+ export type ContentBaseMetricKey = (typeof CONTENT_BASE_METRIC_KEYS)[number];
222
+
223
+ @Schema({ _id: false })
224
+ export class ContentBaseMetrics {
225
+ @Prop({ type: Number, required: false, default: null })
226
+ impressionCount?: number | null;
227
+
228
+ @Prop({ type: Number, required: false, default: null })
229
+ reachCount?: number | null;
230
+
231
+ @Prop({ type: Number, required: false, default: null })
232
+ likeCount?: number | null;
233
+
234
+ @Prop({ type: Number, required: false, default: null })
235
+ commentCount?: number | null;
236
+
237
+ @Prop({ type: Number, required: false, default: null })
238
+ savedCount?: number | null;
239
+
240
+ @Prop({ type: Number, required: false, default: null })
241
+ sharesCount?: number | null;
242
+
243
+ @Prop({ type: Number, required: false, default: null })
244
+ storyCount?: number | null;
245
+
246
+ @Prop({ type: Number, required: false, default: null })
247
+ tapBackCount?: number | null;
248
+
249
+ @Prop({ type: Number, required: false, default: null })
250
+ clickCount?: number | null;
251
+
252
+ @Prop({ type: Number, required: false, default: null })
253
+ replyCount?: number | null;
254
+
255
+ @Prop({ type: Number, required: false, default: null })
256
+ tapForwardCount?: number | null;
257
+
258
+ @Prop({ type: Number, required: false, default: null })
259
+ swipeForwardCount?: number | null;
260
+
261
+ @Prop({ type: Number, required: false, default: null })
262
+ exitCount?: number | null;
263
+
264
+ @Prop({ type: Number, required: false, default: null })
265
+ favoriteCount?: number | null;
266
+
267
+ @Prop({ type: Number, required: false, default: null })
268
+ dislikeCount?: number | null;
269
+
270
+ @Prop({ type: Number, required: false, default: null })
271
+ emv?: number | null;
272
+ }
273
+
274
+ export const ContentBaseMetricsSchema = SchemaFactory.createForClass(ContentBaseMetrics);
275
+
276
+ /** Divisão Orgânico e Impulsionado; o total em Content.metrics é sempre a soma. Ver GD-707. */
277
+ @Schema({ _id: false })
278
+ export class ContentMetricsBreakdown {
279
+ @Prop({ type: ContentBaseMetricsSchema, required: false, default: () => ({}) })
280
+ organic: ContentBaseMetrics;
281
+
282
+ @Prop({ type: ContentBaseMetricsSchema, required: false, default: () => ({}) })
283
+ boosted: ContentBaseMetrics;
284
+ }
285
+
286
+ export const ContentMetricsBreakdownSchema = SchemaFactory.createForClass(ContentMetricsBreakdown);
287
+
288
+ /**
289
+ * Dados do vídeo para o VCR. Views não ficam aqui: impressões e views são
290
+ * sempre Content.metrics.impressionCount. Ver GD-707.
291
+ */
292
+ @Schema({ _id: false })
293
+ export class ContentVideo {
294
+ /** Duração em segundos. */
295
+ @Prop({ type: Number, required: false, default: null })
296
+ duration?: number | null;
297
+
298
+ /** Tempo total assistido, somando todas as visualizações, em segundos. */
299
+ @Prop({ type: Number, required: false, default: null })
300
+ watchTime?: number | null;
301
+
302
+ /** (watchTime ÷ duration) ÷ impressionCount, sem teto. */
303
+ @Prop({ type: Number, required: false, default: null })
304
+ vcr?: number | null;
305
+ }
306
+
307
+ export const ContentVideoSchema = SchemaFactory.createForClass(ContentVideo);
308
+
164
309
  @Schema({ _id: false })
165
310
  export class ContentMetricsHistoryEntry {
166
311
  @Prop({ type: MetricsSchema, required: true })
@@ -330,14 +475,27 @@ export class Content {
330
475
  @Prop({ type: Boolean, required: true, default: false })
331
476
  hasAffiliateLink: boolean;
332
477
 
333
- @Prop([
334
- { type: MongooseSchema.Types.ObjectId, ref: 'Profile', required: false },
335
- ])
336
- collabs?: Types.ObjectId[] | Profile[];
478
+ /** Coautores do post; isCollab = collabs.length > 0. Ver GD-703. */
479
+ @Prop({ type: [ContentCollabSchema], required: false, default: [] })
480
+ collabs?: ContentCollab[];
337
481
 
482
+ /** Métricas do post (sem métrica por pessoa): derivadas com o potentialReach somado. Ver GD-703. */
338
483
  @Prop({ type: MetricsSchema, required: true })
339
484
  metrics: Metrics;
340
485
 
486
+ @Prop({ type: ContentMetricsBreakdownSchema, required: false, default: null })
487
+ metricsBreakdown?: ContentMetricsBreakdown | null;
488
+
489
+ /**
490
+ * Caminhos das partes digitadas à mão (ex.: organic.reachCount,
491
+ * video.watchTime): a captura não sobrescreve. Ver GD-707.
492
+ */
493
+ @Prop({ type: [String], required: false, default: [] })
494
+ manualMetrics?: string[];
495
+
496
+ @Prop({ type: ContentVideoSchema, required: false, default: null })
497
+ video?: ContentVideo | null;
498
+
341
499
  @Prop({ type: MetricsSchema, required: true })
342
500
  estimates: Estimates;
343
501
 
@@ -368,9 +526,11 @@ export class Content {
368
526
  @Prop({ type: String, required: false })
369
527
  mediaUrl?: string;
370
528
 
529
+ /** @deprecated Obsoleto desde a GD-707: a edição vale por parte (manualMetrics). */
371
530
  @Prop({ type: Boolean, required: false, default: false })
372
531
  isEdited?: boolean;
373
532
 
533
+ /** @deprecated Obsoleto desde a GD-707. */
374
534
  @Prop({ type: Date, required: false })
375
535
  editedAt?: Date;
376
536
 
@@ -437,6 +597,8 @@ ContentSchema.index({ createdAt: -1, status: 1 });
437
597
  // Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
438
598
  ContentSchema.index({ primetagId: 1 });
439
599
  ContentSchema.index({ linkKey: 1 });
600
+ // Visão por Influenciador (GD-703): posts em que o Perfil é coautor.
601
+ ContentSchema.index({ 'collabs.profile': 1 });
440
602
  // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
441
603
  // primetagId) ficam fora do índice.
442
604
  ContentSchema.index(
@@ -0,0 +1,51 @@
1
+ import {
2
+ CONTENT_BASE_METRIC_KEYS,
3
+ ContentBaseMetricKey,
4
+ ContentBaseMetrics,
5
+ ContentMetricsBreakdown,
6
+ Metrics,
7
+ } from '../schemas/content.schema';
8
+
9
+ // Partes que, se digitadas à mão, fazem o engajamento virar a soma.
10
+ const ENGAGEMENT_KEYS: ContentBaseMetricKey[] = ['likeCount', 'commentCount', 'sharesCount', 'savedCount'];
11
+
12
+ const sumParts = (organic?: number | null, boosted?: number | null): number | null =>
13
+ organic == null && boosted == null ? null : (organic ?? 0) + (boosted ?? 0);
14
+
15
+ /**
16
+ * Totais de Content.metrics a partir da divisão Orgânico e Impulsionado. Ver GD-707.
17
+ *
18
+ * - metrics.<chave> = organic + boosted (null só se as duas forem null);
19
+ * - engagementCount = o da Primetag, a menos que alguma parte de curtidas,
20
+ * comentários, compartilhamentos ou salvos seja manual: aí vira a soma dos totais;
21
+ * - engagementRate = engagementCount ÷ impressionCount.
22
+ *
23
+ * Usada pela captura e pela edição (api-campaigns): as duas precisam dar o mesmo total.
24
+ */
25
+ export const mergeMetricsBreakdown = (
26
+ breakdown: ContentMetricsBreakdown,
27
+ primetagEngagementCount: number | null | undefined,
28
+ manualMetrics: string[] = [],
29
+ ): Partial<Metrics> => {
30
+ const organic: ContentBaseMetrics = breakdown.organic ?? {};
31
+ const boosted: ContentBaseMetrics = breakdown.boosted ?? {};
32
+
33
+ const totals: Partial<Metrics> = {};
34
+ for (const key of CONTENT_BASE_METRIC_KEYS) {
35
+ totals[key] = sumParts(organic[key], boosted[key]);
36
+ }
37
+
38
+ const hasManualEngagement = ENGAGEMENT_KEYS.some(
39
+ (key) => manualMetrics.includes(`organic.${key}`) || manualMetrics.includes(`boosted.${key}`),
40
+ );
41
+ const engagementCount = hasManualEngagement
42
+ ? ENGAGEMENT_KEYS.reduce((sum, key) => sum + (totals[key] ?? 0), 0)
43
+ : primetagEngagementCount ?? null;
44
+
45
+ const impressionCount = totals.impressionCount ?? 0;
46
+ return {
47
+ ...totals,
48
+ engagementCount,
49
+ engagementRate: engagementCount != null && impressionCount > 0 ? engagementCount / impressionCount : null,
50
+ };
51
+ };