@grapadigital/shared-schemas 1.0.197 → 1.0.198

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);
@@ -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"];
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,29 @@ Estimates = __decorate([
229
249
  ], Estimates);
230
250
  exports.Estimates = Estimates;
231
251
  exports.EstimatesSchema = mongoose_1.SchemaFactory.createForClass(Estimates);
252
+ exports.CONTENT_STATUS_VALUES = ['pending', 'approved', 'deleted', 'processing', 'classified'];
253
+ // Status que entram em relatórios/KPIs (pending e deleted ficam de fora).
254
+ exports.CONTENT_REPORTABLE_STATUS_VALUES = ['approved', 'processing', 'classified'];
255
+ exports.CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'];
256
+ let ContentStatusChangedBy = class ContentStatusChangedBy {
257
+ };
258
+ __decorate([
259
+ (0, mongoose_1.Prop)({ type: String, required: false }),
260
+ __metadata("design:type", String)
261
+ ], ContentStatusChangedBy.prototype, "team", void 0);
262
+ __decorate([
263
+ (0, mongoose_1.Prop)({ type: String, required: false }),
264
+ __metadata("design:type", String)
265
+ ], ContentStatusChangedBy.prototype, "name", void 0);
266
+ __decorate([
267
+ (0, mongoose_1.Prop)({ type: String, required: false }),
268
+ __metadata("design:type", String)
269
+ ], ContentStatusChangedBy.prototype, "email", void 0);
270
+ ContentStatusChangedBy = __decorate([
271
+ (0, mongoose_1.Schema)({ _id: false })
272
+ ], ContentStatusChangedBy);
273
+ exports.ContentStatusChangedBy = ContentStatusChangedBy;
274
+ exports.ContentStatusChangedBySchema = mongoose_1.SchemaFactory.createForClass(ContentStatusChangedBy);
232
275
  let Content = class Content {
233
276
  };
234
277
  __decorate([
@@ -270,12 +313,28 @@ __decorate([
270
313
  __decorate([
271
314
  (0, mongoose_1.Prop)({
272
315
  type: String,
273
- enum: ['approved', 'repproved', 'deleted'],
316
+ enum: exports.CONTENT_STATUS_VALUES,
274
317
  required: true,
275
- default: 'approved',
318
+ default: 'pending',
276
319
  }),
277
320
  __metadata("design:type", String)
278
321
  ], Content.prototype, "status", void 0);
322
+ __decorate([
323
+ (0, mongoose_1.Prop)({ type: String, enum: exports.CONTENT_METRICS_SOURCE_VALUES, required: false, default: null }),
324
+ __metadata("design:type", Object)
325
+ ], Content.prototype, "metricsSource", void 0);
326
+ __decorate([
327
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
328
+ __metadata("design:type", Object)
329
+ ], Content.prototype, "processedCommentCount", void 0);
330
+ __decorate([
331
+ (0, mongoose_1.Prop)({ type: Date, required: false, default: null }),
332
+ __metadata("design:type", Object)
333
+ ], Content.prototype, "approvedAt", void 0);
334
+ __decorate([
335
+ (0, mongoose_1.Prop)({ type: exports.ContentStatusChangedBySchema, required: false, default: null }),
336
+ __metadata("design:type", Object)
337
+ ], Content.prototype, "approvedBy", void 0);
279
338
  __decorate([
280
339
  (0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, ref: 'Profile', required: true }),
281
340
  __metadata("design:type", Object)
@@ -369,4 +428,7 @@ exports.ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
369
428
  exports.ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
370
429
  exports.ContentSchema.index({ profile: 1 });
371
430
  exports.ContentSchema.index({ createdAt: -1, status: 1 });
431
+ // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
432
+ // primetagId) ficam fora do índice.
433
+ exports.ContentSchema.index({ action: 1, primetagId: 1 }, { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } });
372
434
  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.198",
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);
@@ -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,29 @@ export class Estimates {
173
188
 
174
189
  export const EstimatesSchema = SchemaFactory.createForClass(Estimates);
175
190
 
191
+ export const CONTENT_STATUS_VALUES = ['pending', 'approved', 'deleted', 'processing', 'classified'] as const;
192
+ export type ContentStatus = (typeof CONTENT_STATUS_VALUES)[number];
193
+
194
+ // Status que entram em relatórios/KPIs (pending e deleted ficam de fora).
195
+ export const CONTENT_REPORTABLE_STATUS_VALUES: ContentStatus[] = ['approved', 'processing', 'classified'];
196
+
197
+ export const CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'] as const;
198
+ export type ContentMetricsSource = (typeof CONTENT_METRICS_SOURCE_VALUES)[number];
199
+
200
+ @Schema({ _id: false })
201
+ export class ContentStatusChangedBy {
202
+ @Prop({ type: String, required: false })
203
+ team?: string;
204
+
205
+ @Prop({ type: String, required: false })
206
+ name?: string;
207
+
208
+ @Prop({ type: String, required: false })
209
+ email?: string;
210
+ }
211
+
212
+ export const ContentStatusChangedBySchema = SchemaFactory.createForClass(ContentStatusChangedBy);
213
+
176
214
  @Schema({ timestamps: true })
177
215
  export class Content {
178
216
  @Prop({ type: MongooseSchema.Types.ObjectId, auto: true, required: false })
@@ -205,13 +243,35 @@ export class Content {
205
243
  })
206
244
  socialnetwork: 'instagram' | 'tiktok' | 'youtube' | 'pinterest' | 'linkedin' | 'twitter';
207
245
 
246
+ // pending: capturado da Primetag, aguardando aprovação manual.
247
+ // approved: aprovado manualmente, entra no pipeline (comentários → classificação → métricas).
248
+ // processing: dentro do pipeline — nunca é reprocessado nem aprovado/excluído nesse estado.
249
+ // classified: pipeline concluído.
250
+ // deleted: reprovado ou excluído manualmente.
208
251
  @Prop({
209
252
  type: String,
210
- enum: ['approved', 'repproved', 'deleted'],
253
+ enum: CONTENT_STATUS_VALUES,
211
254
  required: true,
212
- default: 'approved',
255
+ default: 'pending',
213
256
  })
214
- status: 'approved' | 'repproved' | 'deleted';
257
+ status: ContentStatus;
258
+
259
+ // Origem das métricas vindas da Primetag (ver metrics_snapshot):
260
+ // real = insights oficiais (perfil conectado), organic = métrica pública,
261
+ // estimated = estimada pela Primetag ou pela nossa estimativa de story.
262
+ @Prop({ type: String, enum: CONTENT_METRICS_SOURCE_VALUES, required: false, default: null })
263
+ metricsSource?: ContentMetricsSource | null;
264
+
265
+ // Contagem de comentários da Primetag no último processamento concluído —
266
+ // decide se há comentário novo pra recapturar/classificar.
267
+ @Prop({ type: Number, required: false, default: null })
268
+ processedCommentCount?: number | null;
269
+
270
+ @Prop({ type: Date, required: false, default: null })
271
+ approvedAt?: Date | null;
272
+
273
+ @Prop({ type: ContentStatusChangedBySchema, required: false, default: null })
274
+ approvedBy?: ContentStatusChangedBy | null;
215
275
 
216
276
  @Prop({ type: MongooseSchema.Types.ObjectId, ref: 'Profile', required: true })
217
277
  profile: Types.ObjectId | Profile;
@@ -288,5 +348,11 @@ ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
288
348
  ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
289
349
  ContentSchema.index({ profile: 1 });
290
350
  ContentSchema.index({ createdAt: -1, status: 1 });
351
+ // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
352
+ // primetagId) ficam fora do índice.
353
+ ContentSchema.index(
354
+ { action: 1, primetagId: 1 },
355
+ { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } },
356
+ );
291
357
 
292
358
  export const ContentModel = MongooseModel('Content', ContentSchema);