@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 +2 -0
- package/dist/index.js +2 -0
- package/dist/schemas/benchmark.schema.d.ts +1 -6
- package/dist/schemas/benchmark.schema.js +3 -8
- package/dist/schemas/content.schema.d.ts +0 -31
- package/dist/schemas/content.schema.js +1 -43
- package/dist/schemas/influencer.schema.d.ts +45 -0
- package/dist/schemas/influencer.schema.js +75 -1
- package/dist/utils/cost-per-impact.util.d.ts +37 -0
- package/dist/utils/cost-per-impact.util.js +72 -0
- package/dist/utils/influencer-contract.util.d.ts +27 -0
- package/dist/utils/influencer-contract.util.js +67 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/schemas/benchmark.schema.ts +2 -7
- package/src/schemas/content.schema.ts +0 -39
- package/src/schemas/influencer.schema.ts +70 -0
- package/src/utils/cost-per-impact.util.ts +99 -0
- package/src/utils/influencer-contract.util.ts +83 -0
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.
|
|
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:
|
|
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.
|
|
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
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:
|
|
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
|
+
};
|