@grapadigital/shared-schemas 1.0.226 → 1.0.228

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
@@ -38,3 +38,5 @@ export * from './schemas/snapshot.schema';
38
38
  export * from './schemas/lead.schema';
39
39
  export * from './utils/normalize-content-link.util';
40
40
  export * from './utils/merge-metrics-breakdown.util';
41
+ export * from './utils/influencer-contract.util';
42
+ export * from './utils/cost-per-impact.util';
package/dist/index.js CHANGED
@@ -59,3 +59,5 @@ __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
61
  __exportStar(require("./utils/merge-metrics-breakdown.util"), exports);
62
+ __exportStar(require("./utils/influencer-contract.util"), exports);
63
+ __exportStar(require("./utils/cost-per-impact.util"), exports);
@@ -25,14 +25,9 @@
25
25
  /// <reference types="mongoose/types/inferrawdoctype" />
26
26
  import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose';
27
27
  import { Client } from './client.schema';
28
- /**
29
- * Métrica do benchmark. Nomes por extenso (GD-751); os antigos (`er`, `cpi`,
30
- * `cpe`) ficam aceitos só durante a virada e saem na fase de remoção.
31
- */
28
+ /** Métrica do benchmark, com nomes por extenso (GD-751). */
32
29
  export declare const BENCHMARK_METRIC_VALUES: readonly ["reach", "engagementRate", "costPerThousandImpressions", "costPerEngagement"];
33
30
  export type BenchmarkMetric = (typeof BENCHMARK_METRIC_VALUES)[number];
34
- /** @deprecated Nomes antigos aceitos só na virada (GD-751). */
35
- export declare const LEGACY_BENCHMARK_METRIC_VALUES: readonly ["er", "cpi", "cpe", "costPerImpression"];
36
31
  export type BenchmarkMode = 'organic' | 'paid';
37
32
  export type BenchmarkDocument = HydratedDocument<Benchmark>;
38
33
  export declare class Benchmark {
@@ -9,16 +9,11 @@ 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.BenchmarkModel = exports.BenchmarkSchema = exports.Benchmark = exports.LEGACY_BENCHMARK_METRIC_VALUES = exports.BENCHMARK_METRIC_VALUES = void 0;
12
+ exports.BenchmarkModel = exports.BenchmarkSchema = exports.Benchmark = exports.BENCHMARK_METRIC_VALUES = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
- /**
16
- * Métrica do benchmark. Nomes por extenso (GD-751); os antigos (`er`, `cpi`,
17
- * `cpe`) ficam aceitos só durante a virada e saem na fase de remoção.
18
- */
15
+ /** Métrica do benchmark, com nomes por extenso (GD-751). */
19
16
  exports.BENCHMARK_METRIC_VALUES = ['reach', 'engagementRate', 'costPerThousandImpressions', 'costPerEngagement'];
20
- /** @deprecated Nomes antigos aceitos só na virada (GD-751). */
21
- exports.LEGACY_BENCHMARK_METRIC_VALUES = ['er', 'cpi', 'cpe', 'costPerImpression'];
22
17
  let Benchmark = class Benchmark {
23
18
  };
24
19
  __decorate([
@@ -34,7 +29,7 @@ __decorate([
34
29
  __metadata("design:type", String)
35
30
  ], Benchmark.prototype, "mode", void 0);
36
31
  __decorate([
37
- (0, mongoose_1.Prop)({ type: String, enum: [...exports.BENCHMARK_METRIC_VALUES, ...exports.LEGACY_BENCHMARK_METRIC_VALUES], required: true }),
32
+ (0, mongoose_1.Prop)({ type: String, enum: exports.BENCHMARK_METRIC_VALUES, required: true }),
38
33
  __metadata("design:type", String)
39
34
  ], Benchmark.prototype, "metric", void 0);
40
35
  __decorate([
@@ -93,8 +93,6 @@ export declare class Metrics {
93
93
  commentCount?: number | null;
94
94
  savedCount?: number | null;
95
95
  shareCount?: number | null;
96
- /** @deprecated Usar `shareCount` (GD-751). Sai na fase de remoção. */
97
- sharesCount?: number | null;
98
96
  storyCount?: number | null;
99
97
  tapBackCount?: number | null;
100
98
  clickCount?: number | null;
@@ -106,8 +104,6 @@ export declare class Metrics {
106
104
  dislikeCount?: number | null;
107
105
  /** Valor de Mídia Estimado (EMV), da Primetag. Ver GD-751. */
108
106
  earnedMediaValue?: number | null;
109
- /** @deprecated Usar `earnedMediaValue` (GD-751). Sai na fase de remoção. */
110
- emv?: number | null;
111
107
  engagementCount?: number | null;
112
108
  engagementRate?: number | null;
113
109
  effectiveReach?: number | null;
@@ -117,16 +113,10 @@ export declare class Metrics {
117
113
  allocatedBudgetAmount?: number | null;
118
114
  /** Custo por Influência (CPMI): investimento ÷ (alcance × Score) × 1000. Ver GD-751. */
119
115
  costPerInfluency?: number | null;
120
- /** @deprecated Usar `costPerInfluency` (GD-751). Sai na fase de remoção. */
121
- cpmi?: number | null;
122
116
  /** Custo por Mil Impressões (CPM): investimento ÷ impressões × 1000. Ver GD-751. */
123
117
  costPerThousandImpressions?: number | null;
124
- /** @deprecated Usar `costPerThousandImpressions` (GD-751). Sai na fase de remoção. */
125
- cpi?: number | null;
126
118
  /** Custo por Engajamento (CPE): investimento ÷ engajamento. Ver GD-751. */
127
119
  costPerEngagement?: number | null;
128
- /** @deprecated Usar `costPerEngagement` (GD-751). Sai na fase de remoção. */
129
- cpe?: number | null;
130
120
  /**
131
121
  * Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
132
122
  * Content.metrics soma o autor e todos os coautores. Ver GD-703.
@@ -177,8 +167,6 @@ export declare class ContentBaseMetrics {
177
167
  commentCount?: number | null;
178
168
  savedCount?: number | null;
179
169
  shareCount?: number | null;
180
- /** @deprecated Usar `shareCount` (GD-751). Sai na fase de remoção. */
181
- sharesCount?: number | null;
182
170
  storyCount?: number | null;
183
171
  tapBackCount?: number | null;
184
172
  clickCount?: number | null;
@@ -190,8 +178,6 @@ export declare class ContentBaseMetrics {
190
178
  dislikeCount?: number | null;
191
179
  /** Valor de Mídia Estimado (EMV), da Primetag. Ver GD-751. */
192
180
  earnedMediaValue?: number | null;
193
- /** @deprecated Usar `earnedMediaValue` (GD-751). Sai na fase de remoção. */
194
- emv?: number | null;
195
181
  }
196
182
  export declare const ContentBaseMetricsSchema: MongooseSchema<ContentBaseMetrics, import("mongoose").Model<ContentBaseMetrics, any, any, any, import("mongoose").Document<unknown, any, ContentBaseMetrics, any, {}> & ContentBaseMetrics & {
197
183
  _id: Types.ObjectId;
@@ -227,8 +213,6 @@ export declare class ContentVideo {
227
213
  watchTime?: number | null;
228
214
  /** Taxa de Conclusão do Vídeo (VCR): (watchTime ÷ duration) ÷ impressionCount, sem teto. Ver GD-751. */
229
215
  videoCompletionRate?: number | null;
230
- /** @deprecated Usar `videoCompletionRate` (GD-751). Sai na fase de remoção. */
231
- vcr?: number | null;
232
216
  }
233
217
  export declare const ContentVideoSchema: MongooseSchema<ContentVideo, import("mongoose").Model<ContentVideo, any, any, any, import("mongoose").Document<unknown, any, ContentVideo, any, {}> & ContentVideo & {
234
218
  _id: Types.ObjectId;
@@ -268,19 +252,6 @@ export declare const EstimatedMetricsSchema: MongooseSchema<EstimatedMetrics, im
268
252
  } & {
269
253
  __v: number;
270
254
  }>;
271
- /** @deprecated Usar `EstimatedMetrics` (GD-751). */
272
- export declare class Estimates extends EstimatedMetrics {
273
- }
274
- /** @deprecated Usar `EstimatedMetricsSchema` (GD-751). */
275
- export declare const EstimatesSchema: MongooseSchema<EstimatedMetrics, import("mongoose").Model<EstimatedMetrics, any, any, any, import("mongoose").Document<unknown, any, EstimatedMetrics, any, {}> & EstimatedMetrics & {
276
- _id: Types.ObjectId;
277
- } & {
278
- __v: number;
279
- }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, EstimatedMetrics, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<EstimatedMetrics>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<EstimatedMetrics> & {
280
- _id: Types.ObjectId;
281
- } & {
282
- __v: number;
283
- }>;
284
255
  export declare const CONTENT_STATUS_VALUES: readonly ["pending", "approved", "deleted", "processing", "classified", "error", "partial_error"];
285
256
  export type ContentStatus = (typeof CONTENT_STATUS_VALUES)[number];
286
257
  export declare const CONTENT_REPORTABLE_STATUS_VALUES: ContentStatus[];
@@ -337,8 +308,6 @@ export declare class Content {
337
308
  manualMetrics?: string[];
338
309
  video?: ContentVideo | null;
339
310
  estimatedMetrics?: EstimatedMetrics | null;
340
- /** @deprecated Usar `estimatedMetrics` (GD-751). Sai na fase de remoção. */
341
- estimates?: Estimates;
342
311
  googleSheetRow?: string;
343
312
  createdAt?: Date;
344
313
  updatedAt?: 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.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.EstimatedMetricsSchema = exports.EstimatedMetrics = 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;
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.EstimatedMetricsSchema = exports.EstimatedMetrics = 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");
@@ -127,10 +127,6 @@ __decorate([
127
127
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
128
128
  __metadata("design:type", Object)
129
129
  ], Metrics.prototype, "shareCount", void 0);
130
- __decorate([
131
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
132
- __metadata("design:type", Object)
133
- ], Metrics.prototype, "sharesCount", void 0);
134
130
  __decorate([
135
131
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
136
132
  __metadata("design:type", Object)
@@ -171,10 +167,6 @@ __decorate([
171
167
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
172
168
  __metadata("design:type", Object)
173
169
  ], Metrics.prototype, "earnedMediaValue", void 0);
174
- __decorate([
175
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
176
- __metadata("design:type", Object)
177
- ], Metrics.prototype, "emv", void 0);
178
170
  __decorate([
179
171
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
180
172
  __metadata("design:type", Object)
@@ -207,26 +199,14 @@ __decorate([
207
199
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
208
200
  __metadata("design:type", Object)
209
201
  ], Metrics.prototype, "costPerInfluency", void 0);
210
- __decorate([
211
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
212
- __metadata("design:type", Object)
213
- ], Metrics.prototype, "cpmi", void 0);
214
202
  __decorate([
215
203
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
216
204
  __metadata("design:type", Object)
217
205
  ], Metrics.prototype, "costPerThousandImpressions", void 0);
218
- __decorate([
219
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
220
- __metadata("design:type", Object)
221
- ], Metrics.prototype, "cpi", void 0);
222
206
  __decorate([
223
207
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
224
208
  __metadata("design:type", Object)
225
209
  ], Metrics.prototype, "costPerEngagement", void 0);
226
- __decorate([
227
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
228
- __metadata("design:type", Object)
229
- ], Metrics.prototype, "cpe", void 0);
230
210
  __decorate([
231
211
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
232
212
  __metadata("design:type", Object)
@@ -313,10 +293,6 @@ __decorate([
313
293
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
314
294
  __metadata("design:type", Object)
315
295
  ], ContentBaseMetrics.prototype, "shareCount", void 0);
316
- __decorate([
317
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
318
- __metadata("design:type", Object)
319
- ], ContentBaseMetrics.prototype, "sharesCount", void 0);
320
296
  __decorate([
321
297
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
322
298
  __metadata("design:type", Object)
@@ -357,10 +333,6 @@ __decorate([
357
333
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
358
334
  __metadata("design:type", Object)
359
335
  ], ContentBaseMetrics.prototype, "earnedMediaValue", void 0);
360
- __decorate([
361
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
362
- __metadata("design:type", Object)
363
- ], ContentBaseMetrics.prototype, "emv", void 0);
364
336
  ContentBaseMetrics = __decorate([
365
337
  (0, mongoose_1.Schema)({ _id: false })
366
338
  ], ContentBaseMetrics);
@@ -400,10 +372,6 @@ __decorate([
400
372
  (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
401
373
  __metadata("design:type", Object)
402
374
  ], ContentVideo.prototype, "videoCompletionRate", void 0);
403
- __decorate([
404
- (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
405
- __metadata("design:type", Object)
406
- ], ContentVideo.prototype, "vcr", void 0);
407
375
  ContentVideo = __decorate([
408
376
  (0, mongoose_1.Schema)({ _id: false })
409
377
  ], ContentVideo);
@@ -448,12 +416,6 @@ EstimatedMetrics = __decorate([
448
416
  ], EstimatedMetrics);
449
417
  exports.EstimatedMetrics = EstimatedMetrics;
450
418
  exports.EstimatedMetricsSchema = mongoose_1.SchemaFactory.createForClass(EstimatedMetrics);
451
- /** @deprecated Usar `EstimatedMetrics` (GD-751). */
452
- class Estimates extends EstimatedMetrics {
453
- }
454
- exports.Estimates = Estimates;
455
- /** @deprecated Usar `EstimatedMetricsSchema` (GD-751). */
456
- exports.EstimatesSchema = exports.EstimatedMetricsSchema;
457
419
  // error: captura de comentários falhou ou a classificação falhou por inteiro.
458
420
  // partial_error: parte dos comentários não foi classificada. Nenhum dos dois
459
421
  // volta sozinho pro pipeline — só por reenvio manual (aprovação).
@@ -618,10 +580,6 @@ __decorate([
618
580
  (0, mongoose_1.Prop)({ type: exports.EstimatedMetricsSchema, required: false, default: null }),
619
581
  __metadata("design:type", Object)
620
582
  ], Content.prototype, "estimatedMetrics", void 0);
621
- __decorate([
622
- (0, mongoose_1.Prop)({ type: exports.MetricsSchema, required: false }),
623
- __metadata("design:type", Estimates)
624
- ], Content.prototype, "estimates", void 0);
625
583
  __decorate([
626
584
  (0, mongoose_1.Prop)({ type: String, required: false }),
627
585
  __metadata("design:type", String)
@@ -62,15 +62,60 @@ export declare const NoteSchema: MongooseSchema<Note, import("mongoose").Model<N
62
62
  } & {
63
63
  __v: number;
64
64
  }>;
65
+ /** Status do Contrato do Influenciador. Ver GD-715. */
66
+ export declare const CONTRACT_STATUS_VALUES: readonly ["draft", "proposed", "signed", "rejected"];
67
+ export type ContractStatus = (typeof CONTRACT_STATUS_VALUES)[number];
68
+ /**
69
+ * Aditivo do Contrato: soma no valor total quando ativo e pode estender a
70
+ * vigência (o fim do Contrato vai até o fim do último Aditivo ativo). Não tem
71
+ * ligação com Ações. Ver GD-715.
72
+ */
73
+ export declare class ContractAmendment {
74
+ _id?: Types.ObjectId;
75
+ value: number;
76
+ isActive?: boolean;
77
+ startDate: Date;
78
+ endDate: Date;
79
+ description?: string | null;
80
+ }
81
+ export declare const ContractAmendmentSchema: MongooseSchema<ContractAmendment, import("mongoose").Model<ContractAmendment, any, any, any, import("mongoose").Document<unknown, any, ContractAmendment, any, {}> & ContractAmendment & Required<{
82
+ _id: Types.ObjectId;
83
+ }> & {
84
+ __v: number;
85
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ContractAmendment, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ContractAmendment>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ContractAmendment> & Required<{
86
+ _id: Types.ObjectId;
87
+ }> & {
88
+ __v: number;
89
+ }>;
65
90
  export declare class InfluencerContract {
66
91
  _id?: Types.ObjectId;
67
92
  client: Types.ObjectId | Client;
93
+ /** Cachê total do Contrato (o `valor_cache` do genc). Ver GD-715. */
68
94
  value: number;
95
+ /** Valor em produtos; entra no total. Ver GD-715. */
96
+ productsValue?: number;
97
+ status?: ContractStatus;
98
+ /** Desligado = Contrato encerrado. */
99
+ isActive?: boolean;
100
+ /**
101
+ * Rateio do custo: monthly = valor mensal (total ÷ meses) por mês-calendário;
102
+ * annual = valor total no período.
103
+ */
69
104
  periodType: 'monthly' | 'annual';
70
105
  startDate: Date;
71
106
  endDate?: Date;
72
107
  deliveryScope?: Delivery;
73
108
  documentUrl?: string;
109
+ /** CPI (Custo por Impacto) contratual, digitado no planejamento. Ver GD-715. */
110
+ contractedCostPerImpact?: number | null;
111
+ /** E/R contratual, em fração (0,02 = 2%). */
112
+ contractedEngagementRate?: number | null;
113
+ /** Ex.: "Team Galaxy", "Content Creator", "Tech". */
114
+ squad?: string | null;
115
+ /** Ex.: "Technology", "Camera", "Fashion". */
116
+ passionPoint?: string | null;
117
+ description?: string | null;
118
+ amendments?: ContractAmendment[];
74
119
  }
75
120
  export declare const InfluencerContractSchema: MongooseSchema<InfluencerContract, import("mongoose").Model<InfluencerContract, any, any, any, import("mongoose").Document<unknown, any, InfluencerContract, any, {}> & InfluencerContract & Required<{
76
121
  _id: Types.ObjectId;
@@ -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.InfluencerModel = exports.InfluencerSchema = exports.Influencer = exports.InfluencerContractSchema = exports.InfluencerContract = exports.NoteSchema = exports.Note = exports.AdvisorSchema = exports.Advisor = void 0;
12
+ exports.InfluencerModel = exports.InfluencerSchema = exports.Influencer = exports.InfluencerContractSchema = exports.InfluencerContract = exports.ContractAmendmentSchema = exports.ContractAmendment = exports.CONTRACT_STATUS_VALUES = exports.NoteSchema = exports.Note = exports.AdvisorSchema = exports.Advisor = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
15
  const party_schema_1 = require("./party.schema");
@@ -72,6 +72,44 @@ Note = __decorate([
72
72
  ], Note);
73
73
  exports.Note = Note;
74
74
  exports.NoteSchema = mongoose_1.SchemaFactory.createForClass(Note);
75
+ /** Status do Contrato do Influenciador. Ver GD-715. */
76
+ exports.CONTRACT_STATUS_VALUES = ['draft', 'proposed', 'signed', 'rejected'];
77
+ /**
78
+ * Aditivo do Contrato: soma no valor total quando ativo e pode estender a
79
+ * vigência (o fim do Contrato vai até o fim do último Aditivo ativo). Não tem
80
+ * ligação com Ações. Ver GD-715.
81
+ */
82
+ let ContractAmendment = class ContractAmendment {
83
+ };
84
+ __decorate([
85
+ (0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, auto: true, required: false }),
86
+ __metadata("design:type", mongoose_2.Types.ObjectId)
87
+ ], ContractAmendment.prototype, "_id", void 0);
88
+ __decorate([
89
+ (0, mongoose_1.Prop)({ type: Number, required: true }),
90
+ __metadata("design:type", Number)
91
+ ], ContractAmendment.prototype, "value", void 0);
92
+ __decorate([
93
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: true }),
94
+ __metadata("design:type", Boolean)
95
+ ], ContractAmendment.prototype, "isActive", void 0);
96
+ __decorate([
97
+ (0, mongoose_1.Prop)({ type: Date, required: true }),
98
+ __metadata("design:type", Date)
99
+ ], ContractAmendment.prototype, "startDate", void 0);
100
+ __decorate([
101
+ (0, mongoose_1.Prop)({ type: Date, required: true }),
102
+ __metadata("design:type", Date)
103
+ ], ContractAmendment.prototype, "endDate", void 0);
104
+ __decorate([
105
+ (0, mongoose_1.Prop)({ type: String, required: false, default: null }),
106
+ __metadata("design:type", Object)
107
+ ], ContractAmendment.prototype, "description", void 0);
108
+ ContractAmendment = __decorate([
109
+ (0, mongoose_1.Schema)({ timestamps: true })
110
+ ], ContractAmendment);
111
+ exports.ContractAmendment = ContractAmendment;
112
+ exports.ContractAmendmentSchema = mongoose_1.SchemaFactory.createForClass(ContractAmendment);
75
113
  // Contrato de valor fixo recorrente (influenciador embaixador de marca) —
76
114
  // alternativa ao pagamento avulso por Recruitment (scope.payment/advisorPayment).
77
115
  // Escopo de entregas reaproveita a mesma classe Delivery do termo de aceite
@@ -90,6 +128,18 @@ __decorate([
90
128
  (0, mongoose_1.Prop)({ type: Number, required: true }),
91
129
  __metadata("design:type", Number)
92
130
  ], InfluencerContract.prototype, "value", void 0);
131
+ __decorate([
132
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: 0 }),
133
+ __metadata("design:type", Number)
134
+ ], InfluencerContract.prototype, "productsValue", void 0);
135
+ __decorate([
136
+ (0, mongoose_1.Prop)({ type: String, enum: exports.CONTRACT_STATUS_VALUES, required: false, default: 'signed' }),
137
+ __metadata("design:type", String)
138
+ ], InfluencerContract.prototype, "status", void 0);
139
+ __decorate([
140
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: true }),
141
+ __metadata("design:type", Boolean)
142
+ ], InfluencerContract.prototype, "isActive", void 0);
93
143
  __decorate([
94
144
  (0, mongoose_1.Prop)({ type: String, enum: ['monthly', 'annual'], required: true }),
95
145
  __metadata("design:type", String)
@@ -110,6 +160,30 @@ __decorate([
110
160
  (0, mongoose_1.Prop)({ type: String, required: false }),
111
161
  __metadata("design:type", String)
112
162
  ], InfluencerContract.prototype, "documentUrl", void 0);
163
+ __decorate([
164
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
165
+ __metadata("design:type", Object)
166
+ ], InfluencerContract.prototype, "contractedCostPerImpact", void 0);
167
+ __decorate([
168
+ (0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
169
+ __metadata("design:type", Object)
170
+ ], InfluencerContract.prototype, "contractedEngagementRate", void 0);
171
+ __decorate([
172
+ (0, mongoose_1.Prop)({ type: String, required: false, default: null }),
173
+ __metadata("design:type", Object)
174
+ ], InfluencerContract.prototype, "squad", void 0);
175
+ __decorate([
176
+ (0, mongoose_1.Prop)({ type: String, required: false, default: null }),
177
+ __metadata("design:type", Object)
178
+ ], InfluencerContract.prototype, "passionPoint", void 0);
179
+ __decorate([
180
+ (0, mongoose_1.Prop)({ type: String, required: false, default: null }),
181
+ __metadata("design:type", Object)
182
+ ], InfluencerContract.prototype, "description", void 0);
183
+ __decorate([
184
+ (0, mongoose_1.Prop)({ type: [exports.ContractAmendmentSchema], required: false, default: [] }),
185
+ __metadata("design:type", Array)
186
+ ], InfluencerContract.prototype, "amendments", void 0);
113
187
  InfluencerContract = __decorate([
114
188
  (0, mongoose_1.Schema)({ timestamps: true })
115
189
  ], InfluencerContract);
@@ -0,0 +1,37 @@
1
+ import { InfluencerContract } from '../schemas/influencer.schema';
2
+ export declare const getEngagementRatePercent: (totals: {
3
+ engagement: number;
4
+ potentialReach: number;
5
+ }) => number | null;
6
+ export declare const getCostPerImpact: (input: {
7
+ value: number;
8
+ engagementRatePercent: number | null;
9
+ impressions: number;
10
+ posts: number;
11
+ }) => number | null;
12
+ export interface ContractMonthTotals {
13
+ year: number;
14
+ month: number;
15
+ posts: number;
16
+ impressions: number;
17
+ engagement: number;
18
+ potentialReach: number;
19
+ }
20
+ export interface MonthlyCostPerImpact extends ContractMonthTotals {
21
+ monthlyValue: number;
22
+ monthlyCostPerImpact: number | null;
23
+ engagementRatePercent: number | null;
24
+ elapsedMonths: number;
25
+ accumulatedSpend: number;
26
+ }
27
+ /**
28
+ * Série mensal acumulada desde o mês de início até `untilMonth` (inclusive):
29
+ * gasto acumulado = valor mensal × meses decorridos; métricas somadas desde o
30
+ * início. `monthlyTotals` traz os totais de cada mês (não acumulados).
31
+ */
32
+ export declare const getMonthlyCostPerImpactSeries: (contract: Pick<InfluencerContract, 'value' | 'productsValue' | 'startDate' | 'endDate' | 'amendments' | 'status' | 'isActive'>, monthlyTotals: ContractMonthTotals[], until: {
33
+ year: number;
34
+ month: number;
35
+ }) => MonthlyCostPerImpact[];
36
+ /** Média dos CPIs mensais não nulos. */
37
+ export declare const getAverageMonthlyCostPerImpact: (series: MonthlyCostPerImpact[]) => number | null;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAverageMonthlyCostPerImpact = exports.getMonthlyCostPerImpactSeries = exports.getCostPerImpact = exports.getEngagementRatePercent = void 0;
4
+ const influencer_contract_util_1 = require("./influencer-contract.util");
5
+ /**
6
+ * CPI (Custo por Impacto), como no genc (`TB_Contratos_Influencer_Cliente` e
7
+ * `TB_CPI_Mensal_Contrato`). Ver GD-715.
8
+ * CPI = valor ÷ ((E/R% ÷ 100) × impressões × posts), E/R% = engajamento ÷ alcance potencial × 100.
9
+ */
10
+ const round = (value, places) => Math.round(value * 10 ** places) / 10 ** places;
11
+ const getEngagementRatePercent = (totals) => totals.potentialReach > 0 ? (totals.engagement / totals.potentialReach) * 100 : null;
12
+ exports.getEngagementRatePercent = getEngagementRatePercent;
13
+ const getCostPerImpact = (input) => {
14
+ const { value, engagementRatePercent, impressions, posts } = input;
15
+ if (!engagementRatePercent || engagementRatePercent <= 0 || value <= 0)
16
+ return null;
17
+ const divisor = (engagementRatePercent / 100) * impressions * posts;
18
+ return divisor > 0 ? round(value / divisor, 6) : null;
19
+ };
20
+ exports.getCostPerImpact = getCostPerImpact;
21
+ /**
22
+ * Série mensal acumulada desde o mês de início até `untilMonth` (inclusive):
23
+ * gasto acumulado = valor mensal × meses decorridos; métricas somadas desde o
24
+ * início. `monthlyTotals` traz os totais de cada mês (não acumulados).
25
+ */
26
+ const getMonthlyCostPerImpactSeries = (contract, monthlyTotals, until) => {
27
+ const brt = new Date(new Date(contract.startDate).getTime() - 3 * 60 * 60 * 1000);
28
+ let year = brt.getUTCFullYear();
29
+ let month = brt.getUTCMonth() + 1;
30
+ const monthlyValue = (0, influencer_contract_util_1.getContractMonthlyValue)(contract);
31
+ const byMonth = new Map(monthlyTotals.map((t) => [`${t.year}-${t.month}`, t]));
32
+ const acc = { posts: 0, impressions: 0, engagement: 0, potentialReach: 0 };
33
+ const series = [];
34
+ let elapsedMonths = 0;
35
+ while (year < until.year || (year === until.year && month <= until.month)) {
36
+ elapsedMonths += 1;
37
+ const totals = byMonth.get(`${year}-${month}`);
38
+ if (totals) {
39
+ acc.posts += totals.posts;
40
+ acc.impressions += totals.impressions;
41
+ acc.engagement += totals.engagement;
42
+ acc.potentialReach += totals.potentialReach;
43
+ }
44
+ const accumulatedSpend = round(monthlyValue * elapsedMonths, 2);
45
+ const engagementRatePercent = acc.posts > 0 ? (0, exports.getEngagementRatePercent)(acc) : null;
46
+ series.push({
47
+ year,
48
+ month,
49
+ ...acc,
50
+ monthlyValue,
51
+ elapsedMonths,
52
+ accumulatedSpend,
53
+ engagementRatePercent: engagementRatePercent == null ? null : round(engagementRatePercent, 4),
54
+ monthlyCostPerImpact: acc.posts > 0 && acc.engagement > 0
55
+ ? (0, exports.getCostPerImpact)({ value: accumulatedSpend, engagementRatePercent, impressions: acc.impressions, posts: acc.posts })
56
+ : null,
57
+ });
58
+ month += 1;
59
+ if (month > 12) {
60
+ month = 1;
61
+ year += 1;
62
+ }
63
+ }
64
+ return series;
65
+ };
66
+ exports.getMonthlyCostPerImpactSeries = getMonthlyCostPerImpactSeries;
67
+ /** Média dos CPIs mensais não nulos. */
68
+ const getAverageMonthlyCostPerImpact = (series) => {
69
+ const values = series.map((row) => row.monthlyCostPerImpact).filter((v) => v != null);
70
+ return values.length > 0 ? round(values.reduce((a, b) => a + b, 0) / values.length, 6) : null;
71
+ };
72
+ exports.getAverageMonthlyCostPerImpact = getAverageMonthlyCostPerImpact;
@@ -0,0 +1,27 @@
1
+ import { InfluencerContract } from '../schemas/influencer.schema';
2
+ type ContractLike = Pick<InfluencerContract, 'value' | 'productsValue' | 'startDate' | 'endDate' | 'amendments' | 'status' | 'isActive'> & {
3
+ client?: unknown;
4
+ };
5
+ /**
6
+ * Fim efetivo da vigência: o maior entre o fim do Contrato e o fim dos Aditivos
7
+ * ativos (no genc o Aditivo estende a vigência). null = Contrato em aberto.
8
+ * Ver GD-715.
9
+ */
10
+ export declare const getContractEndDate: (contract: ContractLike) => Date | null;
11
+ /**
12
+ * Meses de vigência como no genc (`calcular_meses_vigencia`): anos × 12 + meses
13
+ * e mais 1 se sobrar dia; mínimo 1. Sem fim, conta até `now`.
14
+ * Ex.: 10/06/2025 a 11/02/2027 = 21.
15
+ */
16
+ export declare const getContractMonths: (contract: ContractLike, now?: Date) => number;
17
+ /** Cachê + produtos + Aditivos ativos. */
18
+ export declare const getContractTotalValue: (contract: ContractLike) => number;
19
+ /** Total ÷ meses de vigência, 2 casas (428.392,93 ÷ 21 = 20.399,66). */
20
+ export declare const getContractMonthlyValue: (contract: ContractLike, now?: Date) => number;
21
+ /** Ativo e assinado (status ausente conta como assinado). */
22
+ export declare const isContractUsable: (contract: ContractLike) => boolean;
23
+ /** Contrato usável do cliente vigente em `at` (sem fim = em aberto). */
24
+ export declare const getActiveContract: <T extends ContractLike>(influencer: {
25
+ contracts?: T[] | null | undefined;
26
+ } | null | undefined, clientId: unknown, at: Date) => T | null;
27
+ export {};
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getActiveContract = exports.isContractUsable = exports.getContractMonthlyValue = exports.getContractTotalValue = exports.getContractMonths = exports.getContractEndDate = void 0;
4
+ const BRT_OFFSET_MS = 3 * 60 * 60 * 1000;
5
+ /** Data no calendário de São Paulo (as datas do Flow são gravadas à meia-noite BRT = 03:00Z). */
6
+ const brtParts = (date) => {
7
+ const d = new Date(new Date(date).getTime() - BRT_OFFSET_MS);
8
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
9
+ };
10
+ const round2 = (value) => Math.round(value * 100) / 100;
11
+ const activeAmendments = (contract) => (contract.amendments ?? []).filter((amendment) => amendment.isActive !== false);
12
+ /**
13
+ * Fim efetivo da vigência: o maior entre o fim do Contrato e o fim dos Aditivos
14
+ * ativos (no genc o Aditivo estende a vigência). null = Contrato em aberto.
15
+ * Ver GD-715.
16
+ */
17
+ const getContractEndDate = (contract) => {
18
+ const ends = [contract.endDate, ...activeAmendments(contract).map((a) => a.endDate)]
19
+ .filter((date) => !!date)
20
+ .map((date) => new Date(date).getTime());
21
+ return ends.length > 0 ? new Date(Math.max(...ends)) : null;
22
+ };
23
+ exports.getContractEndDate = getContractEndDate;
24
+ /**
25
+ * Meses de vigência como no genc (`calcular_meses_vigencia`): anos × 12 + meses
26
+ * e mais 1 se sobrar dia; mínimo 1. Sem fim, conta até `now`.
27
+ * Ex.: 10/06/2025 a 11/02/2027 = 21.
28
+ */
29
+ const getContractMonths = (contract, now = new Date()) => {
30
+ const start = brtParts(contract.startDate);
31
+ const end = brtParts((0, exports.getContractEndDate)(contract) ?? now);
32
+ let months = (end.year - start.year) * 12 + (end.month - start.month);
33
+ let days = end.day - start.day;
34
+ if (days < 0) {
35
+ months -= 1;
36
+ days += 1; // relativedelta: só importa se sobrou dia
37
+ }
38
+ if (days > 0)
39
+ months += 1;
40
+ return Math.max(1, months);
41
+ };
42
+ exports.getContractMonths = getContractMonths;
43
+ /** Cachê + produtos + Aditivos ativos. */
44
+ const getContractTotalValue = (contract) => round2((contract.value ?? 0) +
45
+ (contract.productsValue ?? 0) +
46
+ activeAmendments(contract).reduce((sum, a) => sum + (a.value ?? 0), 0));
47
+ exports.getContractTotalValue = getContractTotalValue;
48
+ /** Total ÷ meses de vigência, 2 casas (428.392,93 ÷ 21 = 20.399,66). */
49
+ const getContractMonthlyValue = (contract, now) => round2((0, exports.getContractTotalValue)(contract) / (0, exports.getContractMonths)(contract, now));
50
+ exports.getContractMonthlyValue = getContractMonthlyValue;
51
+ /** Ativo e assinado (status ausente conta como assinado). */
52
+ const isContractUsable = (contract) => contract.isActive !== false && (contract.status ?? 'signed') === 'signed';
53
+ exports.isContractUsable = isContractUsable;
54
+ /** Contrato usável do cliente vigente em `at` (sem fim = em aberto). */
55
+ const getActiveContract = (influencer, clientId, at) => {
56
+ const time = new Date(at).getTime();
57
+ return ((influencer?.contracts ?? []).find((contract) => {
58
+ if (!(0, exports.isContractUsable)(contract))
59
+ return false;
60
+ const client = contract.client?._id ?? contract.client;
61
+ if (String(client) !== String(clientId))
62
+ return false;
63
+ const end = (0, exports.getContractEndDate)(contract);
64
+ return new Date(contract.startDate).getTime() <= time && (!end || time <= end.getTime());
65
+ }) ?? null);
66
+ };
67
+ exports.getActiveContract = getActiveContract;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grapadigital/shared-schemas",
3
- "version": "1.0.226",
3
+ "version": "1.0.228",
4
4
  "description": "Shared Mongoose Schemas",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -38,3 +38,5 @@ export * from './schemas/snapshot.schema';
38
38
  export * from './schemas/lead.schema';
39
39
  export * from './utils/normalize-content-link.util';
40
40
  export * from './utils/merge-metrics-breakdown.util';
41
+ export * from './utils/influencer-contract.util';
42
+ export * from './utils/cost-per-impact.util';
@@ -2,14 +2,9 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
2
  import { HydratedDocument, Schema as MongooseSchema, Types, model as MongooseModel } from 'mongoose';
3
3
  import { Client } from './client.schema';
4
4
 
5
- /**
6
- * Métrica do benchmark. Nomes por extenso (GD-751); os antigos (`er`, `cpi`,
7
- * `cpe`) ficam aceitos só durante a virada e saem na fase de remoção.
8
- */
5
+ /** Métrica do benchmark, com nomes por extenso (GD-751). */
9
6
  export const BENCHMARK_METRIC_VALUES = ['reach', 'engagementRate', 'costPerThousandImpressions', 'costPerEngagement'] as const;
10
7
  export type BenchmarkMetric = (typeof BENCHMARK_METRIC_VALUES)[number];
11
- /** @deprecated Nomes antigos aceitos só na virada (GD-751). */
12
- export const LEGACY_BENCHMARK_METRIC_VALUES = ['er', 'cpi', 'cpe', 'costPerImpression'] as const;
13
8
  export type BenchmarkMode = 'organic' | 'paid';
14
9
  export type BenchmarkDocument = HydratedDocument<Benchmark>;
15
10
 
@@ -24,7 +19,7 @@ export class Benchmark {
24
19
  @Prop({ type: String, enum: ['organic', 'paid'], required: true })
25
20
  mode: BenchmarkMode;
26
21
 
27
- @Prop({ type: String, enum: [...BENCHMARK_METRIC_VALUES, ...LEGACY_BENCHMARK_METRIC_VALUES], required: true })
22
+ @Prop({ type: String, enum: BENCHMARK_METRIC_VALUES, required: true })
28
23
  metric: BenchmarkMetric;
29
24
 
30
25
  @Prop({ type: Number, required: true })
@@ -96,10 +96,6 @@ export class Metrics {
96
96
  @Prop({ type: Number, required: false, default: null })
97
97
  shareCount?: number | null;
98
98
 
99
- /** @deprecated Usar `shareCount` (GD-751). Sai na fase de remoção. */
100
- @Prop({ type: Number, required: false, default: null })
101
- sharesCount?: number | null;
102
-
103
99
  @Prop({ type: Number, required: false, default: null })
104
100
  storyCount?: number | null;
105
101
 
@@ -131,10 +127,6 @@ export class Metrics {
131
127
  @Prop({ type: Number, required: false, default: null })
132
128
  earnedMediaValue?: number | null;
133
129
 
134
- /** @deprecated Usar `earnedMediaValue` (GD-751). Sai na fase de remoção. */
135
- @Prop({ type: Number, required: false, default: null })
136
- emv?: number | null;
137
-
138
130
  @Prop({ type: Number, required: false, default: null })
139
131
  engagementCount?: number | null;
140
132
 
@@ -162,26 +154,14 @@ export class Metrics {
162
154
  @Prop({ type: Number, required: false, default: null })
163
155
  costPerInfluency?: number | null;
164
156
 
165
- /** @deprecated Usar `costPerInfluency` (GD-751). Sai na fase de remoção. */
166
- @Prop({ type: Number, required: false, default: null })
167
- cpmi?: number | null;
168
-
169
157
  /** Custo por Mil Impressões (CPM): investimento ÷ impressões × 1000. Ver GD-751. */
170
158
  @Prop({ type: Number, required: false, default: null })
171
159
  costPerThousandImpressions?: number | null;
172
160
 
173
- /** @deprecated Usar `costPerThousandImpressions` (GD-751). Sai na fase de remoção. */
174
- @Prop({ type: Number, required: false, default: null })
175
- cpi?: number | null;
176
-
177
161
  /** Custo por Engajamento (CPE): investimento ÷ engajamento. Ver GD-751. */
178
162
  @Prop({ type: Number, required: false, default: null })
179
163
  costPerEngagement?: number | null;
180
164
 
181
- /** @deprecated Usar `costPerEngagement` (GD-751). Sai na fase de remoção. */
182
- @Prop({ type: Number, required: false, default: null })
183
- cpe?: number | null;
184
-
185
165
  /**
186
166
  * Alcance Potencial (o antigo PO Reach): seguidores no momento do post. Em
187
167
  * Content.metrics soma o autor e todos os coautores. Ver GD-703.
@@ -264,10 +244,6 @@ export class ContentBaseMetrics {
264
244
  @Prop({ type: Number, required: false, default: null })
265
245
  shareCount?: number | null;
266
246
 
267
- /** @deprecated Usar `shareCount` (GD-751). Sai na fase de remoção. */
268
- @Prop({ type: Number, required: false, default: null })
269
- sharesCount?: number | null;
270
-
271
247
  @Prop({ type: Number, required: false, default: null })
272
248
  storyCount?: number | null;
273
249
 
@@ -299,9 +275,6 @@ export class ContentBaseMetrics {
299
275
  @Prop({ type: Number, required: false, default: null })
300
276
  earnedMediaValue?: number | null;
301
277
 
302
- /** @deprecated Usar `earnedMediaValue` (GD-751). Sai na fase de remoção. */
303
- @Prop({ type: Number, required: false, default: null })
304
- emv?: number | null;
305
278
  }
306
279
 
307
280
  export const ContentBaseMetricsSchema = SchemaFactory.createForClass(ContentBaseMetrics);
@@ -336,9 +309,6 @@ export class ContentVideo {
336
309
  @Prop({ type: Number, required: false, default: null })
337
310
  videoCompletionRate?: number | null;
338
311
 
339
- /** @deprecated Usar `videoCompletionRate` (GD-751). Sai na fase de remoção. */
340
- @Prop({ type: Number, required: false, default: null })
341
- vcr?: number | null;
342
312
  }
343
313
 
344
314
  export const ContentVideoSchema = SchemaFactory.createForClass(ContentVideo);
@@ -372,11 +342,6 @@ export class EstimatedMetrics {
372
342
 
373
343
  export const EstimatedMetricsSchema = SchemaFactory.createForClass(EstimatedMetrics);
374
344
 
375
- /** @deprecated Usar `EstimatedMetrics` (GD-751). */
376
- export class Estimates extends EstimatedMetrics {}
377
- /** @deprecated Usar `EstimatedMetricsSchema` (GD-751). */
378
- export const EstimatesSchema = EstimatedMetricsSchema;
379
-
380
345
  // error: captura de comentários falhou ou a classificação falhou por inteiro.
381
346
  // partial_error: parte dos comentários não foi classificada. Nenhum dos dois
382
347
  // volta sozinho pro pipeline — só por reenvio manual (aprovação).
@@ -542,10 +507,6 @@ export class Content {
542
507
  @Prop({ type: EstimatedMetricsSchema, required: false, default: null })
543
508
  estimatedMetrics?: EstimatedMetrics | null;
544
509
 
545
- /** @deprecated Usar `estimatedMetrics` (GD-751). Sai na fase de remoção. */
546
- @Prop({ type: MetricsSchema, required: false })
547
- estimates?: Estimates;
548
-
549
510
  @Prop({ type: String, required: false })
550
511
  googleSheetRow?: string;
551
512
 
@@ -47,6 +47,38 @@ export class Note {
47
47
 
48
48
  export const NoteSchema = SchemaFactory.createForClass(Note);
49
49
 
50
+ /** Status do Contrato do Influenciador. Ver GD-715. */
51
+ export const CONTRACT_STATUS_VALUES = ['draft', 'proposed', 'signed', 'rejected'] as const;
52
+ export type ContractStatus = (typeof CONTRACT_STATUS_VALUES)[number];
53
+
54
+ /**
55
+ * Aditivo do Contrato: soma no valor total quando ativo e pode estender a
56
+ * vigência (o fim do Contrato vai até o fim do último Aditivo ativo). Não tem
57
+ * ligação com Ações. Ver GD-715.
58
+ */
59
+ @Schema({ timestamps: true })
60
+ export class ContractAmendment {
61
+ @Prop({ type: MongooseSchema.Types.ObjectId, auto: true, required: false })
62
+ _id?: Types.ObjectId;
63
+
64
+ @Prop({ type: Number, required: true })
65
+ value: number;
66
+
67
+ @Prop({ type: Boolean, required: false, default: true })
68
+ isActive?: boolean;
69
+
70
+ @Prop({ type: Date, required: true })
71
+ startDate: Date;
72
+
73
+ @Prop({ type: Date, required: true })
74
+ endDate: Date;
75
+
76
+ @Prop({ type: String, required: false, default: null })
77
+ description?: string | null;
78
+ }
79
+
80
+ export const ContractAmendmentSchema = SchemaFactory.createForClass(ContractAmendment);
81
+
50
82
  // Contrato de valor fixo recorrente (influenciador embaixador de marca) —
51
83
  // alternativa ao pagamento avulso por Recruitment (scope.payment/advisorPayment).
52
84
  // Escopo de entregas reaproveita a mesma classe Delivery do termo de aceite
@@ -59,9 +91,25 @@ export class InfluencerContract {
59
91
  @Prop({ type: MongooseSchema.Types.ObjectId, ref: 'Client', required: true })
60
92
  client: Types.ObjectId | Client;
61
93
 
94
+ /** Cachê total do Contrato (o `valor_cache` do genc). Ver GD-715. */
62
95
  @Prop({ type: Number, required: true })
63
96
  value: number;
64
97
 
98
+ /** Valor em produtos; entra no total. Ver GD-715. */
99
+ @Prop({ type: Number, required: false, default: 0 })
100
+ productsValue?: number;
101
+
102
+ @Prop({ type: String, enum: CONTRACT_STATUS_VALUES, required: false, default: 'signed' })
103
+ status?: ContractStatus;
104
+
105
+ /** Desligado = Contrato encerrado. */
106
+ @Prop({ type: Boolean, required: false, default: true })
107
+ isActive?: boolean;
108
+
109
+ /**
110
+ * Rateio do custo: monthly = valor mensal (total ÷ meses) por mês-calendário;
111
+ * annual = valor total no período.
112
+ */
65
113
  @Prop({ type: String, enum: ['monthly', 'annual'], required: true })
66
114
  periodType: 'monthly' | 'annual';
67
115
 
@@ -83,6 +131,28 @@ export class InfluencerContract {
83
131
  // fica salva no Mongo.
84
132
  @Prop({ type: String, required: false })
85
133
  documentUrl?: string;
134
+
135
+ /** CPI (Custo por Impacto) contratual, digitado no planejamento. Ver GD-715. */
136
+ @Prop({ type: Number, required: false, default: null })
137
+ contractedCostPerImpact?: number | null;
138
+
139
+ /** E/R contratual, em fração (0,02 = 2%). */
140
+ @Prop({ type: Number, required: false, default: null })
141
+ contractedEngagementRate?: number | null;
142
+
143
+ /** Ex.: "Team Galaxy", "Content Creator", "Tech". */
144
+ @Prop({ type: String, required: false, default: null })
145
+ squad?: string | null;
146
+
147
+ /** Ex.: "Technology", "Camera", "Fashion". */
148
+ @Prop({ type: String, required: false, default: null })
149
+ passionPoint?: string | null;
150
+
151
+ @Prop({ type: String, required: false, default: null })
152
+ description?: string | null;
153
+
154
+ @Prop({ type: [ContractAmendmentSchema], required: false, default: [] })
155
+ amendments?: ContractAmendment[];
86
156
  }
87
157
 
88
158
  export const InfluencerContractSchema = SchemaFactory.createForClass(InfluencerContract);
@@ -0,0 +1,99 @@
1
+ import { getContractMonthlyValue } from './influencer-contract.util';
2
+ import { InfluencerContract } from '../schemas/influencer.schema';
3
+
4
+ /**
5
+ * CPI (Custo por Impacto), como no genc (`TB_Contratos_Influencer_Cliente` e
6
+ * `TB_CPI_Mensal_Contrato`). Ver GD-715.
7
+ * CPI = valor ÷ ((E/R% ÷ 100) × impressões × posts), E/R% = engajamento ÷ alcance potencial × 100.
8
+ */
9
+
10
+ const round = (value: number, places: number) => Math.round(value * 10 ** places) / 10 ** places;
11
+
12
+ export const getEngagementRatePercent = (totals: { engagement: number; potentialReach: number }): number | null =>
13
+ totals.potentialReach > 0 ? (totals.engagement / totals.potentialReach) * 100 : null;
14
+
15
+ export const getCostPerImpact = (input: {
16
+ value: number;
17
+ engagementRatePercent: number | null;
18
+ impressions: number;
19
+ posts: number;
20
+ }): number | null => {
21
+ const { value, engagementRatePercent, impressions, posts } = input;
22
+ if (!engagementRatePercent || engagementRatePercent <= 0 || value <= 0) return null;
23
+ const divisor = (engagementRatePercent / 100) * impressions * posts;
24
+ return divisor > 0 ? round(value / divisor, 6) : null;
25
+ };
26
+
27
+ export interface ContractMonthTotals {
28
+ year: number;
29
+ month: number;
30
+ posts: number;
31
+ impressions: number;
32
+ engagement: number;
33
+ potentialReach: number;
34
+ }
35
+
36
+ export interface MonthlyCostPerImpact extends ContractMonthTotals {
37
+ monthlyValue: number;
38
+ monthlyCostPerImpact: number | null;
39
+ engagementRatePercent: number | null;
40
+ elapsedMonths: number;
41
+ accumulatedSpend: number;
42
+ }
43
+
44
+ /**
45
+ * Série mensal acumulada desde o mês de início até `untilMonth` (inclusive):
46
+ * gasto acumulado = valor mensal × meses decorridos; métricas somadas desde o
47
+ * início. `monthlyTotals` traz os totais de cada mês (não acumulados).
48
+ */
49
+ export const getMonthlyCostPerImpactSeries = (
50
+ contract: Pick<InfluencerContract, 'value' | 'productsValue' | 'startDate' | 'endDate' | 'amendments' | 'status' | 'isActive'>,
51
+ monthlyTotals: ContractMonthTotals[],
52
+ until: { year: number; month: number },
53
+ ): MonthlyCostPerImpact[] => {
54
+ const brt = new Date(new Date(contract.startDate).getTime() - 3 * 60 * 60 * 1000);
55
+ let year = brt.getUTCFullYear();
56
+ let month = brt.getUTCMonth() + 1;
57
+ const monthlyValue = getContractMonthlyValue(contract);
58
+ const byMonth = new Map(monthlyTotals.map((t) => [`${t.year}-${t.month}`, t]));
59
+ const acc = { posts: 0, impressions: 0, engagement: 0, potentialReach: 0 };
60
+ const series: MonthlyCostPerImpact[] = [];
61
+ let elapsedMonths = 0;
62
+ while (year < until.year || (year === until.year && month <= until.month)) {
63
+ elapsedMonths += 1;
64
+ const totals = byMonth.get(`${year}-${month}`);
65
+ if (totals) {
66
+ acc.posts += totals.posts;
67
+ acc.impressions += totals.impressions;
68
+ acc.engagement += totals.engagement;
69
+ acc.potentialReach += totals.potentialReach;
70
+ }
71
+ const accumulatedSpend = round(monthlyValue * elapsedMonths, 2);
72
+ const engagementRatePercent = acc.posts > 0 ? getEngagementRatePercent(acc) : null;
73
+ series.push({
74
+ year,
75
+ month,
76
+ ...acc,
77
+ monthlyValue,
78
+ elapsedMonths,
79
+ accumulatedSpend,
80
+ engagementRatePercent: engagementRatePercent == null ? null : round(engagementRatePercent, 4),
81
+ monthlyCostPerImpact:
82
+ acc.posts > 0 && acc.engagement > 0
83
+ ? getCostPerImpact({ value: accumulatedSpend, engagementRatePercent, impressions: acc.impressions, posts: acc.posts })
84
+ : null,
85
+ });
86
+ month += 1;
87
+ if (month > 12) {
88
+ month = 1;
89
+ year += 1;
90
+ }
91
+ }
92
+ return series;
93
+ };
94
+
95
+ /** Média dos CPIs mensais não nulos. */
96
+ export const getAverageMonthlyCostPerImpact = (series: MonthlyCostPerImpact[]): number | null => {
97
+ const values = series.map((row) => row.monthlyCostPerImpact).filter((v): v is number => v != null);
98
+ return values.length > 0 ? round(values.reduce((a, b) => a + b, 0) / values.length, 6) : null;
99
+ };
@@ -0,0 +1,83 @@
1
+ import { InfluencerContract } from '../schemas/influencer.schema';
2
+
3
+ type ContractLike = Pick<
4
+ InfluencerContract,
5
+ 'value' | 'productsValue' | 'startDate' | 'endDate' | 'amendments' | 'status' | 'isActive'
6
+ > & { client?: unknown };
7
+
8
+ const BRT_OFFSET_MS = 3 * 60 * 60 * 1000;
9
+
10
+ /** Data no calendário de São Paulo (as datas do Flow são gravadas à meia-noite BRT = 03:00Z). */
11
+ const brtParts = (date: Date) => {
12
+ const d = new Date(new Date(date).getTime() - BRT_OFFSET_MS);
13
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
14
+ };
15
+
16
+ const round2 = (value: number) => Math.round(value * 100) / 100;
17
+
18
+ const activeAmendments = (contract: ContractLike) =>
19
+ (contract.amendments ?? []).filter((amendment) => amendment.isActive !== false);
20
+
21
+ /**
22
+ * Fim efetivo da vigência: o maior entre o fim do Contrato e o fim dos Aditivos
23
+ * ativos (no genc o Aditivo estende a vigência). null = Contrato em aberto.
24
+ * Ver GD-715.
25
+ */
26
+ export const getContractEndDate = (contract: ContractLike): Date | null => {
27
+ const ends = [contract.endDate, ...activeAmendments(contract).map((a) => a.endDate)]
28
+ .filter((date): date is Date => !!date)
29
+ .map((date) => new Date(date).getTime());
30
+ return ends.length > 0 ? new Date(Math.max(...ends)) : null;
31
+ };
32
+
33
+ /**
34
+ * Meses de vigência como no genc (`calcular_meses_vigencia`): anos × 12 + meses
35
+ * e mais 1 se sobrar dia; mínimo 1. Sem fim, conta até `now`.
36
+ * Ex.: 10/06/2025 a 11/02/2027 = 21.
37
+ */
38
+ export const getContractMonths = (contract: ContractLike, now: Date = new Date()): number => {
39
+ const start = brtParts(contract.startDate);
40
+ const end = brtParts(getContractEndDate(contract) ?? now);
41
+ let months = (end.year - start.year) * 12 + (end.month - start.month);
42
+ let days = end.day - start.day;
43
+ if (days < 0) {
44
+ months -= 1;
45
+ days += 1; // relativedelta: só importa se sobrou dia
46
+ }
47
+ if (days > 0) months += 1;
48
+ return Math.max(1, months);
49
+ };
50
+
51
+ /** Cachê + produtos + Aditivos ativos. */
52
+ export const getContractTotalValue = (contract: ContractLike): number =>
53
+ round2(
54
+ (contract.value ?? 0) +
55
+ (contract.productsValue ?? 0) +
56
+ activeAmendments(contract).reduce((sum, a) => sum + (a.value ?? 0), 0),
57
+ );
58
+
59
+ /** Total ÷ meses de vigência, 2 casas (428.392,93 ÷ 21 = 20.399,66). */
60
+ export const getContractMonthlyValue = (contract: ContractLike, now?: Date): number =>
61
+ round2(getContractTotalValue(contract) / getContractMonths(contract, now));
62
+
63
+ /** Ativo e assinado (status ausente conta como assinado). */
64
+ export const isContractUsable = (contract: ContractLike): boolean =>
65
+ contract.isActive !== false && (contract.status ?? 'signed') === 'signed';
66
+
67
+ /** Contrato usável do cliente vigente em `at` (sem fim = em aberto). */
68
+ export const getActiveContract = <T extends ContractLike>(
69
+ influencer: { contracts?: T[] | null } | null | undefined,
70
+ clientId: unknown,
71
+ at: Date,
72
+ ): T | null => {
73
+ const time = new Date(at).getTime();
74
+ return (
75
+ (influencer?.contracts ?? []).find((contract) => {
76
+ if (!isContractUsable(contract)) return false;
77
+ const client = (contract.client as { _id?: unknown })?._id ?? contract.client;
78
+ if (String(client) !== String(clientId)) return false;
79
+ const end = getContractEndDate(contract);
80
+ return new Date(contract.startDate).getTime() <= time && (!end || time <= end.getTime());
81
+ }) ?? null
82
+ );
83
+ };