@grapadigital/shared-schemas 1.0.197 → 1.0.199

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.
@@ -122,7 +122,14 @@ export declare class Client extends Party {
122
122
  * time já resolve (padrão da maioria dos clientes). Ver GD-532/GD-535.
123
123
  */
124
124
  requiredTeams: string[];
125
- hasReport: boolean;
125
+ /**
126
+ * Análise GenIA: captura de comentários, classificação por IA e tudo que
127
+ * depende deles (spam, taxa de engajamento qualificado, score, CPMI). Sem
128
+ * isso, os conteúdos do cliente só recebem as métricas base da Primetag.
129
+ * Também define quais clientes aparecem no hub de relatórios GenIA.
130
+ * (Antigo `hasReport`.)
131
+ */
132
+ hasGenIAAnalysis: boolean;
126
133
  reportResponsable?: Responsable;
127
134
  }
128
135
  export declare const ClientSchema: import("mongoose").Schema<Client, import("mongoose").Model<Client, any, any, any, import("mongoose").Document<unknown, any, Client, any, {}> & Client & Required<{
@@ -164,7 +164,7 @@ __decorate([
164
164
  __decorate([
165
165
  (0, mongoose_1.Prop)({ type: Boolean, default: false }),
166
166
  __metadata("design:type", Boolean)
167
- ], Client.prototype, "hasReport", void 0);
167
+ ], Client.prototype, "hasGenIAAnalysis", void 0);
168
168
  __decorate([
169
169
  (0, mongoose_1.Prop)({ type: exports.ResponsableSchema, required: false }),
170
170
  __metadata("design:type", Responsable)
@@ -33,6 +33,37 @@ export declare const COMMENT_SUBJECT_VALUES: readonly ["compliment_to_person", "
33
33
  export type CommentSubject = (typeof COMMENT_SUBJECT_VALUES)[number];
34
34
  export declare const COMMENT_CLASSIFICATION_STATUS_VALUES: readonly ["pending", "processing", "completed", "failed", "not_applicable"];
35
35
  export type CommentClassificationStatus = (typeof COMMENT_CLASSIFICATION_STATUS_VALUES)[number];
36
+ export declare const COMMENT_SPAM_RULE_VALUES: readonly ["duplicate_text", "repeat_author", "manual", "unknown"];
37
+ export type CommentSpamRule = (typeof COMMENT_SPAM_RULE_VALUES)[number];
38
+ export declare class CommentSpamReason {
39
+ rule: CommentSpamRule;
40
+ threshold?: number;
41
+ matchCount?: number;
42
+ groupKey?: string;
43
+ }
44
+ export declare const CommentSpamReasonSchema: MongooseSchema<CommentSpamReason, import("mongoose").Model<CommentSpamReason, any, any, any, import("mongoose").Document<unknown, any, CommentSpamReason, any, {}> & CommentSpamReason & {
45
+ _id: Types.ObjectId;
46
+ } & {
47
+ __v: number;
48
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, CommentSpamReason, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<CommentSpamReason>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<CommentSpamReason> & {
49
+ _id: Types.ObjectId;
50
+ } & {
51
+ __v: number;
52
+ }>;
53
+ export declare class CommentClassifiedBy {
54
+ team?: string;
55
+ name?: string;
56
+ email?: string;
57
+ }
58
+ export declare const CommentClassifiedBySchema: MongooseSchema<CommentClassifiedBy, import("mongoose").Model<CommentClassifiedBy, any, any, any, import("mongoose").Document<unknown, any, CommentClassifiedBy, any, {}> & CommentClassifiedBy & {
59
+ _id: Types.ObjectId;
60
+ } & {
61
+ __v: number;
62
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, CommentClassifiedBy, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<CommentClassifiedBy>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<CommentClassifiedBy> & {
63
+ _id: Types.ObjectId;
64
+ } & {
65
+ __v: number;
66
+ }>;
36
67
  export declare class Comment {
37
68
  _id?: Types.ObjectId;
38
69
  content: Types.ObjectId | Content;
@@ -52,6 +83,11 @@ export declare class Comment {
52
83
  funnel?: 'awareness' | 'consideration' | 'conversion' | 'none' | null;
53
84
  isEmojiOnly?: boolean;
54
85
  isSpam?: boolean;
86
+ spamReason?: CommentSpamReason | null;
87
+ spamOverride?: boolean;
88
+ isManuallyClassified?: boolean;
89
+ manuallyClassifiedAt?: Date | null;
90
+ manuallyClassifiedBy?: CommentClassifiedBy | null;
55
91
  classificationStatus?: CommentClassificationStatus;
56
92
  classificationError?: string | null;
57
93
  classificationProcessedAt?: Date | null;
@@ -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.CommentModel = exports.CommentSchema = exports.Comment = exports.COMMENT_CLASSIFICATION_STATUS_VALUES = exports.COMMENT_SUBJECT_VALUES = exports.COMMENT_TARGET_VALUES = void 0;
12
+ exports.CommentModel = exports.CommentSchema = exports.Comment = exports.CommentClassifiedBySchema = exports.CommentClassifiedBy = exports.CommentSpamReasonSchema = exports.CommentSpamReason = exports.COMMENT_SPAM_RULE_VALUES = exports.COMMENT_CLASSIFICATION_STATUS_VALUES = exports.COMMENT_SUBJECT_VALUES = exports.COMMENT_TARGET_VALUES = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
15
  exports.COMMENT_TARGET_VALUES = ['brand', 'influencer', 'content', 'emoji', 'spam'];
@@ -37,6 +37,52 @@ exports.COMMENT_CLASSIFICATION_STATUS_VALUES = [
37
37
  'failed',
38
38
  'not_applicable',
39
39
  ];
40
+ exports.COMMENT_SPAM_RULE_VALUES = ['duplicate_text', 'repeat_author', 'manual', 'unknown'];
41
+ // Por que o comentário foi marcado como spam. `groupKey` identifica os outros
42
+ // comentários do mesmo conteúdo que caíram na mesma regra: autor normalizado
43
+ // (repeat_author) ou `autor::texto` normalizado (duplicate_text).
44
+ let CommentSpamReason = class CommentSpamReason {
45
+ };
46
+ __decorate([
47
+ (0, mongoose_1.Prop)({ type: String, enum: exports.COMMENT_SPAM_RULE_VALUES, required: true }),
48
+ __metadata("design:type", String)
49
+ ], CommentSpamReason.prototype, "rule", void 0);
50
+ __decorate([
51
+ (0, mongoose_1.Prop)({ type: Number, required: false }),
52
+ __metadata("design:type", Number)
53
+ ], CommentSpamReason.prototype, "threshold", void 0);
54
+ __decorate([
55
+ (0, mongoose_1.Prop)({ type: Number, required: false }),
56
+ __metadata("design:type", Number)
57
+ ], CommentSpamReason.prototype, "matchCount", void 0);
58
+ __decorate([
59
+ (0, mongoose_1.Prop)({ type: String, required: false }),
60
+ __metadata("design:type", String)
61
+ ], CommentSpamReason.prototype, "groupKey", void 0);
62
+ CommentSpamReason = __decorate([
63
+ (0, mongoose_1.Schema)({ _id: false })
64
+ ], CommentSpamReason);
65
+ exports.CommentSpamReason = CommentSpamReason;
66
+ exports.CommentSpamReasonSchema = mongoose_1.SchemaFactory.createForClass(CommentSpamReason);
67
+ let CommentClassifiedBy = class CommentClassifiedBy {
68
+ };
69
+ __decorate([
70
+ (0, mongoose_1.Prop)({ type: String, required: false }),
71
+ __metadata("design:type", String)
72
+ ], CommentClassifiedBy.prototype, "team", void 0);
73
+ __decorate([
74
+ (0, mongoose_1.Prop)({ type: String, required: false }),
75
+ __metadata("design:type", String)
76
+ ], CommentClassifiedBy.prototype, "name", void 0);
77
+ __decorate([
78
+ (0, mongoose_1.Prop)({ type: String, required: false }),
79
+ __metadata("design:type", String)
80
+ ], CommentClassifiedBy.prototype, "email", void 0);
81
+ CommentClassifiedBy = __decorate([
82
+ (0, mongoose_1.Schema)({ _id: false })
83
+ ], CommentClassifiedBy);
84
+ exports.CommentClassifiedBy = CommentClassifiedBy;
85
+ exports.CommentClassifiedBySchema = mongoose_1.SchemaFactory.createForClass(CommentClassifiedBy);
40
86
  let Comment = class Comment {
41
87
  };
42
88
  __decorate([
@@ -131,6 +177,26 @@ __decorate([
131
177
  (0, mongoose_1.Prop)({ type: Boolean, required: false, default: false }),
132
178
  __metadata("design:type", Boolean)
133
179
  ], Comment.prototype, "isSpam", void 0);
180
+ __decorate([
181
+ (0, mongoose_1.Prop)({ type: exports.CommentSpamReasonSchema, required: false, default: null }),
182
+ __metadata("design:type", Object)
183
+ ], Comment.prototype, "spamReason", void 0);
184
+ __decorate([
185
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: false }),
186
+ __metadata("design:type", Boolean)
187
+ ], Comment.prototype, "spamOverride", void 0);
188
+ __decorate([
189
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: false }),
190
+ __metadata("design:type", Boolean)
191
+ ], Comment.prototype, "isManuallyClassified", void 0);
192
+ __decorate([
193
+ (0, mongoose_1.Prop)({ type: Date, required: false, default: null }),
194
+ __metadata("design:type", Object)
195
+ ], Comment.prototype, "manuallyClassifiedAt", void 0);
196
+ __decorate([
197
+ (0, mongoose_1.Prop)({ type: exports.CommentClassifiedBySchema, required: false, default: null }),
198
+ __metadata("design:type", Object)
199
+ ], Comment.prototype, "manuallyClassifiedBy", void 0);
134
200
  __decorate([
135
201
  (0, mongoose_1.Prop)({
136
202
  type: String,
@@ -174,4 +240,5 @@ exports.CommentSchema.index({ action: 1, funnel: 1 });
174
240
  exports.CommentSchema.index({ action: 1, target: 1 });
175
241
  exports.CommentSchema.index({ content: 1, parent: 1 });
176
242
  exports.CommentSchema.index({ content: 1, classificationStatus: 1 });
243
+ exports.CommentSchema.index({ content: 1, 'spamReason.groupKey': 1 });
177
244
  exports.CommentModel = (0, mongoose_2.model)('Comment', exports.CommentSchema);
@@ -60,7 +60,7 @@ export declare const ContentCaptureProcessInfluencerSchema: MongooseSchema<Conte
60
60
  __v: number;
61
61
  }>;
62
62
  export declare class ContentCaptureProcessCommentCaptureStatus {
63
- status: 'captured' | 'skipped_cooldown' | 'skipped_network' | 'out_of_scope' | 'failed';
63
+ status: 'captured' | 'skipped_cooldown' | 'skipped_network' | 'skipped_no_comments' | 'out_of_scope' | 'failed';
64
64
  commentsCaptured?: number;
65
65
  error?: string;
66
66
  }
@@ -75,7 +75,7 @@ let ContentCaptureProcessCommentCaptureStatus = class ContentCaptureProcessComme
75
75
  __decorate([
76
76
  (0, mongoose_1.Prop)({
77
77
  type: String,
78
- enum: ['captured', 'skipped_cooldown', 'skipped_network', 'out_of_scope', 'failed'],
78
+ enum: ['captured', 'skipped_cooldown', 'skipped_network', 'skipped_no_comments', 'out_of_scope', 'failed'],
79
79
  required: true,
80
80
  }),
81
81
  __metadata("design:type", String)
@@ -97,6 +97,11 @@ export declare class Metrics {
97
97
  tapBackCount?: number | null;
98
98
  clickCount?: number | null;
99
99
  replyCount?: number | null;
100
+ tapForwardCount?: number | null;
101
+ swipeForwardCount?: number | null;
102
+ exitCount?: number | null;
103
+ favoriteCount?: number | null;
104
+ dislikeCount?: number | null;
100
105
  emv?: number | null;
101
106
  engagementCount?: number | null;
102
107
  engagementRate?: number | null;
@@ -146,6 +151,25 @@ export declare const EstimatesSchema: MongooseSchema<Estimates, import("mongoose
146
151
  } & {
147
152
  __v: number;
148
153
  }>;
154
+ export declare const CONTENT_STATUS_VALUES: readonly ["pending", "approved", "deleted", "processing", "classified", "error", "partial_error"];
155
+ export type ContentStatus = (typeof CONTENT_STATUS_VALUES)[number];
156
+ export declare const CONTENT_REPORTABLE_STATUS_VALUES: ContentStatus[];
157
+ export declare const CONTENT_METRICS_SOURCE_VALUES: readonly ["real", "organic", "estimated"];
158
+ export type ContentMetricsSource = (typeof CONTENT_METRICS_SOURCE_VALUES)[number];
159
+ export declare class ContentStatusChangedBy {
160
+ team?: string;
161
+ name?: string;
162
+ email?: string;
163
+ }
164
+ export declare const ContentStatusChangedBySchema: MongooseSchema<ContentStatusChangedBy, import("mongoose").Model<ContentStatusChangedBy, any, any, any, import("mongoose").Document<unknown, any, ContentStatusChangedBy, any, {}> & ContentStatusChangedBy & {
165
+ _id: Types.ObjectId;
166
+ } & {
167
+ __v: number;
168
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContentStatusChangedBy, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContentStatusChangedBy>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContentStatusChangedBy> & {
169
+ _id: Types.ObjectId;
170
+ } & {
171
+ __v: number;
172
+ }>;
149
173
  export declare class Content {
150
174
  _id?: Types.ObjectId;
151
175
  caption?: string;
@@ -153,7 +177,11 @@ export declare class Content {
153
177
  type: 'reel' | 'carousel' | 'image' | 'story' | 'video' | 'post' | 'pin' | 'tweet';
154
178
  tagType: 'withTag' | 'withoutTag' | 'brandExposure';
155
179
  socialnetwork: 'instagram' | 'tiktok' | 'youtube' | 'pinterest' | 'linkedin' | 'twitter';
156
- status: 'approved' | 'repproved' | 'deleted';
180
+ status: ContentStatus;
181
+ metricsSource?: ContentMetricsSource | null;
182
+ processedCommentCount?: number | null;
183
+ approvedAt?: Date | null;
184
+ approvedBy?: ContentStatusChangedBy | null;
157
185
  profile: Types.ObjectId | Profile;
158
186
  action: Types.ObjectId | Action;
159
187
  postedAt: Date;
@@ -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.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_METRICS_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;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
15
  let SentimentComment = class SentimentComment {
@@ -142,6 +142,26 @@ __decorate([
142
142
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
143
143
  __metadata("design:type", Object)
144
144
  ], Metrics.prototype, "replyCount", void 0);
145
+ __decorate([
146
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
147
+ __metadata("design:type", Object)
148
+ ], Metrics.prototype, "tapForwardCount", void 0);
149
+ __decorate([
150
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
151
+ __metadata("design:type", Object)
152
+ ], Metrics.prototype, "swipeForwardCount", void 0);
153
+ __decorate([
154
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
155
+ __metadata("design:type", Object)
156
+ ], Metrics.prototype, "exitCount", void 0);
157
+ __decorate([
158
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
159
+ __metadata("design:type", Object)
160
+ ], Metrics.prototype, "favoriteCount", void 0);
161
+ __decorate([
162
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
163
+ __metadata("design:type", Object)
164
+ ], Metrics.prototype, "dislikeCount", void 0);
145
165
  __decorate([
146
166
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
147
167
  __metadata("design:type", Object)
@@ -229,6 +249,48 @@ Estimates = __decorate([
229
249
  ], Estimates);
230
250
  exports.Estimates = Estimates;
231
251
  exports.EstimatesSchema = mongoose_1.SchemaFactory.createForClass(Estimates);
252
+ // error: captura de comentários falhou ou a classificação falhou por inteiro.
253
+ // partial_error: parte dos comentários não foi classificada. Nenhum dos dois
254
+ // volta sozinho pro pipeline — só por reenvio manual (aprovação).
255
+ exports.CONTENT_STATUS_VALUES = [
256
+ 'pending',
257
+ 'approved',
258
+ 'deleted',
259
+ 'processing',
260
+ 'classified',
261
+ 'error',
262
+ 'partial_error',
263
+ ];
264
+ // Status que entram em relatórios/KPIs (pending e deleted ficam de fora). error
265
+ // e partial_error entram: as métricas da Primetag são válidas, só os
266
+ // comentários ficaram incompletos.
267
+ exports.CONTENT_REPORTABLE_STATUS_VALUES = [
268
+ 'approved',
269
+ 'processing',
270
+ 'classified',
271
+ 'error',
272
+ 'partial_error',
273
+ ];
274
+ exports.CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'];
275
+ let ContentStatusChangedBy = class ContentStatusChangedBy {
276
+ };
277
+ __decorate([
278
+ (0, mongoose_1.Prop)({ type: String, required: false }),
279
+ __metadata("design:type", String)
280
+ ], ContentStatusChangedBy.prototype, "team", void 0);
281
+ __decorate([
282
+ (0, mongoose_1.Prop)({ type: String, required: false }),
283
+ __metadata("design:type", String)
284
+ ], ContentStatusChangedBy.prototype, "name", void 0);
285
+ __decorate([
286
+ (0, mongoose_1.Prop)({ type: String, required: false }),
287
+ __metadata("design:type", String)
288
+ ], ContentStatusChangedBy.prototype, "email", void 0);
289
+ ContentStatusChangedBy = __decorate([
290
+ (0, mongoose_1.Schema)({ _id: false })
291
+ ], ContentStatusChangedBy);
292
+ exports.ContentStatusChangedBy = ContentStatusChangedBy;
293
+ exports.ContentStatusChangedBySchema = mongoose_1.SchemaFactory.createForClass(ContentStatusChangedBy);
232
294
  let Content = class Content {
233
295
  };
234
296
  __decorate([
@@ -270,12 +332,28 @@ __decorate([
270
332
  __decorate([
271
333
  (0, mongoose_1.Prop)({
272
334
  type: String,
273
- enum: ['approved', 'repproved', 'deleted'],
335
+ enum: exports.CONTENT_STATUS_VALUES,
274
336
  required: true,
275
- default: 'approved',
337
+ default: 'pending',
276
338
  }),
277
339
  __metadata("design:type", String)
278
340
  ], Content.prototype, "status", void 0);
341
+ __decorate([
342
+ (0, mongoose_1.Prop)({ type: String, enum: exports.CONTENT_METRICS_SOURCE_VALUES, required: false, default: null }),
343
+ __metadata("design:type", Object)
344
+ ], Content.prototype, "metricsSource", void 0);
345
+ __decorate([
346
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
347
+ __metadata("design:type", Object)
348
+ ], Content.prototype, "processedCommentCount", void 0);
349
+ __decorate([
350
+ (0, mongoose_1.Prop)({ type: Date, required: false, default: null }),
351
+ __metadata("design:type", Object)
352
+ ], Content.prototype, "approvedAt", void 0);
353
+ __decorate([
354
+ (0, mongoose_1.Prop)({ type: exports.ContentStatusChangedBySchema, required: false, default: null }),
355
+ __metadata("design:type", Object)
356
+ ], Content.prototype, "approvedBy", void 0);
279
357
  __decorate([
280
358
  (0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: true }),
281
359
  __metadata("design:type", Object)
@@ -369,4 +447,7 @@ exports.ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
369
447
  exports.ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
370
448
  exports.ContentSchema.index({ profile: 1 });
371
449
  exports.ContentSchema.index({ createdAt: -1, status: 1 });
450
+ // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
451
+ // primetagId) ficam fora do índice.
452
+ exports.ContentSchema.index({ action: 1, primetagId: 1 }, { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } });
372
453
  exports.ContentModel = (0, mongoose_2.model)('Content', exports.ContentSchema);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grapadigital/shared-schemas",
3
- "version": "1.0.197",
3
+ "version": "1.0.199",
4
4
  "description": "Shared Mongoose Schemas",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -121,8 +121,15 @@ export class Client extends Party {
121
121
  @Prop({ type: [String], default: [] })
122
122
  requiredTeams: string[];
123
123
 
124
+ /**
125
+ * Análise GenIA: captura de comentários, classificação por IA e tudo que
126
+ * depende deles (spam, taxa de engajamento qualificado, score, CPMI). Sem
127
+ * isso, os conteúdos do cliente só recebem as métricas base da Primetag.
128
+ * Também define quais clientes aparecem no hub de relatórios GenIA.
129
+ * (Antigo `hasReport`.)
130
+ */
124
131
  @Prop({ type: Boolean, default: false })
125
- hasReport: boolean;
132
+ hasGenIAAnalysis: boolean;
126
133
 
127
134
  @Prop({ type: ResponsableSchema, required: false })
128
135
  reportResponsable?: Responsable;
@@ -41,6 +41,43 @@ export const COMMENT_CLASSIFICATION_STATUS_VALUES = [
41
41
  ] as const;
42
42
  export type CommentClassificationStatus = (typeof COMMENT_CLASSIFICATION_STATUS_VALUES)[number];
43
43
 
44
+ export const COMMENT_SPAM_RULE_VALUES = ['duplicate_text', 'repeat_author', 'manual', 'unknown'] as const;
45
+ export type CommentSpamRule = (typeof COMMENT_SPAM_RULE_VALUES)[number];
46
+
47
+ // Por que o comentário foi marcado como spam. `groupKey` identifica os outros
48
+ // comentários do mesmo conteúdo que caíram na mesma regra: autor normalizado
49
+ // (repeat_author) ou `autor::texto` normalizado (duplicate_text).
50
+ @Schema({ _id: false })
51
+ export class CommentSpamReason {
52
+ @Prop({ type: String, enum: COMMENT_SPAM_RULE_VALUES, required: true })
53
+ rule: CommentSpamRule;
54
+
55
+ @Prop({ type: Number, required: false })
56
+ threshold?: number;
57
+
58
+ @Prop({ type: Number, required: false })
59
+ matchCount?: number;
60
+
61
+ @Prop({ type: String, required: false })
62
+ groupKey?: string;
63
+ }
64
+
65
+ export const CommentSpamReasonSchema = SchemaFactory.createForClass(CommentSpamReason);
66
+
67
+ @Schema({ _id: false })
68
+ export class CommentClassifiedBy {
69
+ @Prop({ type: String, required: false })
70
+ team?: string;
71
+
72
+ @Prop({ type: String, required: false })
73
+ name?: string;
74
+
75
+ @Prop({ type: String, required: false })
76
+ email?: string;
77
+ }
78
+
79
+ export const CommentClassifiedBySchema = SchemaFactory.createForClass(CommentClassifiedBy);
80
+
44
81
  @Schema({ timestamps: true })
45
82
  export class Comment {
46
83
  @Prop({ type: MongooseSchema.Types.ObjectId, auto: true, required: false })
@@ -117,6 +154,25 @@ export class Comment {
117
154
  @Prop({ type: Boolean, required: false, default: false })
118
155
  isSpam?: boolean;
119
156
 
157
+ @Prop({ type: CommentSpamReasonSchema, required: false, default: null })
158
+ spamReason?: CommentSpamReason | null;
159
+
160
+ // Tirado do spam manualmente: as regras automáticas de spam ignoram esse
161
+ // comentário (e ele sai da contagem do grupo), mas a IA continua classificando.
162
+ @Prop({ type: Boolean, required: false, default: false })
163
+ spamOverride?: boolean;
164
+
165
+ // Classificação ajustada manualmente: nenhum worker sobrescreve
166
+ // target/subject/polarity/funnel/isSpam desse comentário.
167
+ @Prop({ type: Boolean, required: false, default: false })
168
+ isManuallyClassified?: boolean;
169
+
170
+ @Prop({ type: Date, required: false, default: null })
171
+ manuallyClassifiedAt?: Date | null;
172
+
173
+ @Prop({ type: CommentClassifiedBySchema, required: false, default: null })
174
+ manuallyClassifiedBy?: CommentClassifiedBy | null;
175
+
120
176
  @Prop({
121
177
  type: String,
122
178
  enum: COMMENT_CLASSIFICATION_STATUS_VALUES,
@@ -155,5 +211,6 @@ CommentSchema.index({ action: 1, funnel: 1 });
155
211
  CommentSchema.index({ action: 1, target: 1 });
156
212
  CommentSchema.index({ content: 1, parent: 1 });
157
213
  CommentSchema.index({ content: 1, classificationStatus: 1 });
214
+ CommentSchema.index({ content: 1, 'spamReason.groupKey': 1 });
158
215
 
159
216
  export const CommentModel = MongooseModel('Comment', CommentSchema);
@@ -60,10 +60,10 @@ export const ContentCaptureProcessInfluencerSchema = SchemaFactory.createForClas
60
60
  export class ContentCaptureProcessCommentCaptureStatus {
61
61
  @Prop({
62
62
  type: String,
63
- enum: ['captured', 'skipped_cooldown', 'skipped_network', 'out_of_scope', 'failed'],
63
+ enum: ['captured', 'skipped_cooldown', 'skipped_network', 'skipped_no_comments', 'out_of_scope', 'failed'],
64
64
  required: true,
65
65
  })
66
- status: 'captured' | 'skipped_cooldown' | 'skipped_network' | 'out_of_scope' | 'failed';
66
+ status: 'captured' | 'skipped_cooldown' | 'skipped_network' | 'skipped_no_comments' | 'out_of_scope' | 'failed';
67
67
 
68
68
  @Prop({ type: Number, required: false })
69
69
  commentsCaptured?: number;
@@ -107,6 +107,21 @@ export class Metrics {
107
107
  @Prop({ type: Number, required: false, default: null })
108
108
  replyCount?: number | null;
109
109
 
110
+ @Prop({ type: Number, required: false, default: null })
111
+ tapForwardCount?: number | null;
112
+
113
+ @Prop({ type: Number, required: false, default: null })
114
+ swipeForwardCount?: number | null;
115
+
116
+ @Prop({ type: Number, required: false, default: null })
117
+ exitCount?: number | null;
118
+
119
+ @Prop({ type: Number, required: false, default: null })
120
+ favoriteCount?: number | null;
121
+
122
+ @Prop({ type: Number, required: false, default: null })
123
+ dislikeCount?: number | null;
124
+
110
125
  @Prop({ type: Number, required: false, default: null })
111
126
  emv?: number | null;
112
127
 
@@ -173,6 +188,48 @@ export class Estimates {
173
188
 
174
189
  export const EstimatesSchema = SchemaFactory.createForClass(Estimates);
175
190
 
191
+ // error: captura de comentários falhou ou a classificação falhou por inteiro.
192
+ // partial_error: parte dos comentários não foi classificada. Nenhum dos dois
193
+ // volta sozinho pro pipeline — só por reenvio manual (aprovação).
194
+ export const CONTENT_STATUS_VALUES = [
195
+ 'pending',
196
+ 'approved',
197
+ 'deleted',
198
+ 'processing',
199
+ 'classified',
200
+ 'error',
201
+ 'partial_error',
202
+ ] as const;
203
+ export type ContentStatus = (typeof CONTENT_STATUS_VALUES)[number];
204
+
205
+ // Status que entram em relatórios/KPIs (pending e deleted ficam de fora). error
206
+ // e partial_error entram: as métricas da Primetag são válidas, só os
207
+ // comentários ficaram incompletos.
208
+ export const CONTENT_REPORTABLE_STATUS_VALUES: ContentStatus[] = [
209
+ 'approved',
210
+ 'processing',
211
+ 'classified',
212
+ 'error',
213
+ 'partial_error',
214
+ ];
215
+
216
+ export const CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'] as const;
217
+ export type ContentMetricsSource = (typeof CONTENT_METRICS_SOURCE_VALUES)[number];
218
+
219
+ @Schema({ _id: false })
220
+ export class ContentStatusChangedBy {
221
+ @Prop({ type: String, required: false })
222
+ team?: string;
223
+
224
+ @Prop({ type: String, required: false })
225
+ name?: string;
226
+
227
+ @Prop({ type: String, required: false })
228
+ email?: string;
229
+ }
230
+
231
+ export const ContentStatusChangedBySchema = SchemaFactory.createForClass(ContentStatusChangedBy);
232
+
176
233
  @Schema({ timestamps: true })
177
234
  export class Content {
178
235
  @Prop({ type: MongooseSchema.Types.ObjectId, auto: true, required: false })
@@ -205,13 +262,35 @@ export class Content {
205
262
  })
206
263
  socialnetwork: 'instagram' | 'tiktok' | 'youtube' | 'pinterest' | 'linkedin' | 'twitter';
207
264
 
265
+ // pending: capturado da Primetag, aguardando aprovação manual.
266
+ // approved: aprovado manualmente, entra no pipeline (comentários → classificação → métricas).
267
+ // processing: dentro do pipeline — nunca é reprocessado nem aprovado/excluído nesse estado.
268
+ // classified: pipeline concluído.
269
+ // deleted: reprovado ou excluído manualmente.
208
270
  @Prop({
209
271
  type: String,
210
- enum: ['approved', 'repproved', 'deleted'],
272
+ enum: CONTENT_STATUS_VALUES,
211
273
  required: true,
212
- default: 'approved',
274
+ default: 'pending',
213
275
  })
214
- status: 'approved' | 'repproved' | 'deleted';
276
+ status: ContentStatus;
277
+
278
+ // Origem das métricas vindas da Primetag (ver metrics_snapshot):
279
+ // real = insights oficiais (perfil conectado), organic = métrica pública,
280
+ // estimated = estimada pela Primetag ou pela nossa estimativa de story.
281
+ @Prop({ type: String, enum: CONTENT_METRICS_SOURCE_VALUES, required: false, default: null })
282
+ metricsSource?: ContentMetricsSource | null;
283
+
284
+ // Contagem de comentários da Primetag no último processamento concluído —
285
+ // decide se há comentário novo pra recapturar/classificar.
286
+ @Prop({ type: Number, required: false, default: null })
287
+ processedCommentCount?: number | null;
288
+
289
+ @Prop({ type: Date, required: false, default: null })
290
+ approvedAt?: Date | null;
291
+
292
+ @Prop({ type: ContentStatusChangedBySchema, required: false, default: null })
293
+ approvedBy?: ContentStatusChangedBy | null;
215
294
 
216
295
  @Prop({ type: MongooseSchema.Types.ObjectId, ref: 'Profile', required: true })
217
296
  profile: Types.ObjectId | Profile;
@@ -288,5 +367,11 @@ ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
288
367
  ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
289
368
  ContentSchema.index({ profile: 1 });
290
369
  ContentSchema.index({ createdAt: -1, status: 1 });
370
+ // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
371
+ // primetagId) ficam fora do índice.
372
+ ContentSchema.index(
373
+ { action: 1, primetagId: 1 },
374
+ { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } },
375
+ );
291
376
 
292
377
  export const ContentModel = MongooseModel('Content', ContentSchema);