@grapadigital/shared-schemas 1.0.227 → 1.0.230
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/schemas/content.schema.js +5 -4
- 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 +32 -0
- package/dist/utils/influencer-contract.util.js +77 -0
- package/package.json +1 -1
- package/src/schemas/content.schema.ts +5 -4
- package/src/schemas/influencer.schema.ts +70 -0
|
@@ -393,22 +393,23 @@ ContentMetricsHistoryEntry = __decorate([
|
|
|
393
393
|
exports.ContentMetricsHistoryEntry = ContentMetricsHistoryEntry;
|
|
394
394
|
exports.ContentMetricsHistoryEntrySchema = mongoose_1.SchemaFactory.createForClass(ContentMetricsHistoryEntry);
|
|
395
395
|
/** Métricas estimadas pelo porte do perfil, para quando a rede não informa. Ver GD-751. */
|
|
396
|
+
// null é valor válido (sem estimativa): required recusaria null no create.
|
|
396
397
|
let EstimatedMetrics = class EstimatedMetrics {
|
|
397
398
|
};
|
|
398
399
|
__decorate([
|
|
399
|
-
(0, mongoose_1.Prop)({ type: Number, required:
|
|
400
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
400
401
|
__metadata("design:type", Object)
|
|
401
402
|
], EstimatedMetrics.prototype, "impressionCount", void 0);
|
|
402
403
|
__decorate([
|
|
403
|
-
(0, mongoose_1.Prop)({ type: Number, required:
|
|
404
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
404
405
|
__metadata("design:type", Object)
|
|
405
406
|
], EstimatedMetrics.prototype, "reachCount", void 0);
|
|
406
407
|
__decorate([
|
|
407
|
-
(0, mongoose_1.Prop)({ type: Number, required:
|
|
408
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
408
409
|
__metadata("design:type", Object)
|
|
409
410
|
], EstimatedMetrics.prototype, "engagementCount", void 0);
|
|
410
411
|
__decorate([
|
|
411
|
-
(0, mongoose_1.Prop)({ type: Number, required:
|
|
412
|
+
(0, mongoose_1.Prop)({ type: Number, required: false, default: null }),
|
|
412
413
|
__metadata("design:type", Object)
|
|
413
414
|
], EstimatedMetrics.prototype, "engagementRate", void 0);
|
|
414
415
|
EstimatedMetrics = __decorate([
|
|
@@ -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,32 @@
|
|
|
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 (o Aditivo estende a vigência). null = Contrato em aberto, mesmo com
|
|
8
|
+
* Aditivo. Ver GD-715.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getContractEndDate: (contract: ContractLike) => Date | null;
|
|
11
|
+
/**
|
|
12
|
+
* Início do dia seguinte ao fim da vigência em São Paulo (limite exclusivo):
|
|
13
|
+
* o último dia conta inteiro. null = Contrato em aberto.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getContractEndExclusive: (contract: ContractLike) => Date | null;
|
|
16
|
+
/**
|
|
17
|
+
* Meses de vigência como no genc (`calcular_meses_vigencia`): anos × 12 + meses
|
|
18
|
+
* e mais 1 se sobrar dia; mínimo 1. Sem fim, conta até `now`.
|
|
19
|
+
* Ex.: 10/06/2025 a 11/02/2027 = 21; 15/03 a 14/09 = 6.
|
|
20
|
+
*/
|
|
21
|
+
export declare const getContractMonths: (contract: ContractLike, now?: Date) => number;
|
|
22
|
+
/** Cachê + produtos + Aditivos ativos. */
|
|
23
|
+
export declare const getContractTotalValue: (contract: ContractLike) => number;
|
|
24
|
+
/** Total ÷ meses de vigência, 2 casas (428.392,93 ÷ 21 = 20.399,66). */
|
|
25
|
+
export declare const getContractMonthlyValue: (contract: ContractLike, now?: Date) => number;
|
|
26
|
+
/** Ativo e assinado (status ausente conta como assinado). */
|
|
27
|
+
export declare const isContractUsable: (contract: ContractLike) => boolean;
|
|
28
|
+
/** Contrato usável do cliente vigente em `at`, com o último dia inteiro (sem fim = em aberto). */
|
|
29
|
+
export declare const getActiveContract: <T extends ContractLike>(influencer: {
|
|
30
|
+
contracts?: T[] | null | undefined;
|
|
31
|
+
} | null | undefined, clientId: unknown, at: Date) => T | null;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getActiveContract = exports.isContractUsable = exports.getContractMonthlyValue = exports.getContractTotalValue = exports.getContractMonths = exports.getContractEndExclusive = 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 (o Aditivo estende a vigência). null = Contrato em aberto, mesmo com
|
|
15
|
+
* Aditivo. Ver GD-715.
|
|
16
|
+
*/
|
|
17
|
+
const getContractEndDate = (contract) => {
|
|
18
|
+
if (!contract.endDate)
|
|
19
|
+
return null;
|
|
20
|
+
const ends = [contract.endDate, ...activeAmendments(contract).map((a) => a.endDate)]
|
|
21
|
+
.filter((date) => !!date)
|
|
22
|
+
.map((date) => new Date(date).getTime());
|
|
23
|
+
return new Date(Math.max(...ends));
|
|
24
|
+
};
|
|
25
|
+
exports.getContractEndDate = getContractEndDate;
|
|
26
|
+
/**
|
|
27
|
+
* Início do dia seguinte ao fim da vigência em São Paulo (limite exclusivo):
|
|
28
|
+
* o último dia conta inteiro. null = Contrato em aberto.
|
|
29
|
+
*/
|
|
30
|
+
const getContractEndExclusive = (contract) => {
|
|
31
|
+
const end = (0, exports.getContractEndDate)(contract);
|
|
32
|
+
if (!end)
|
|
33
|
+
return null;
|
|
34
|
+
const { year, month, day } = brtParts(end);
|
|
35
|
+
return new Date(Date.UTC(year, month - 1, day + 1) + BRT_OFFSET_MS);
|
|
36
|
+
};
|
|
37
|
+
exports.getContractEndExclusive = getContractEndExclusive;
|
|
38
|
+
/**
|
|
39
|
+
* Meses de vigência como no genc (`calcular_meses_vigencia`): anos × 12 + meses
|
|
40
|
+
* e mais 1 se sobrar dia; mínimo 1. Sem fim, conta até `now`.
|
|
41
|
+
* Ex.: 10/06/2025 a 11/02/2027 = 21; 15/03 a 14/09 = 6.
|
|
42
|
+
*/
|
|
43
|
+
const getContractMonths = (contract, now = new Date()) => {
|
|
44
|
+
const start = brtParts(contract.startDate);
|
|
45
|
+
const end = brtParts((0, exports.getContractEndDate)(contract) ?? now);
|
|
46
|
+
let months = (end.year - start.year) * 12 + (end.month - start.month);
|
|
47
|
+
// relativedelta: dia do fim menor tira um mês e sobra dia, que devolve o mês.
|
|
48
|
+
if (end.day > start.day)
|
|
49
|
+
months += 1;
|
|
50
|
+
return Math.max(1, months);
|
|
51
|
+
};
|
|
52
|
+
exports.getContractMonths = getContractMonths;
|
|
53
|
+
/** Cachê + produtos + Aditivos ativos. */
|
|
54
|
+
const getContractTotalValue = (contract) => round2((contract.value ?? 0) +
|
|
55
|
+
(contract.productsValue ?? 0) +
|
|
56
|
+
activeAmendments(contract).reduce((sum, a) => sum + (a.value ?? 0), 0));
|
|
57
|
+
exports.getContractTotalValue = getContractTotalValue;
|
|
58
|
+
/** Total ÷ meses de vigência, 2 casas (428.392,93 ÷ 21 = 20.399,66). */
|
|
59
|
+
const getContractMonthlyValue = (contract, now) => round2((0, exports.getContractTotalValue)(contract) / (0, exports.getContractMonths)(contract, now));
|
|
60
|
+
exports.getContractMonthlyValue = getContractMonthlyValue;
|
|
61
|
+
/** Ativo e assinado (status ausente conta como assinado). */
|
|
62
|
+
const isContractUsable = (contract) => contract.isActive !== false && (contract.status ?? 'signed') === 'signed';
|
|
63
|
+
exports.isContractUsable = isContractUsable;
|
|
64
|
+
/** Contrato usável do cliente vigente em `at`, com o último dia inteiro (sem fim = em aberto). */
|
|
65
|
+
const getActiveContract = (influencer, clientId, at) => {
|
|
66
|
+
const time = new Date(at).getTime();
|
|
67
|
+
return ((influencer?.contracts ?? []).find((contract) => {
|
|
68
|
+
if (!(0, exports.isContractUsable)(contract))
|
|
69
|
+
return false;
|
|
70
|
+
const client = contract.client?._id ?? contract.client;
|
|
71
|
+
if (String(client) !== String(clientId))
|
|
72
|
+
return false;
|
|
73
|
+
const end = (0, exports.getContractEndExclusive)(contract);
|
|
74
|
+
return new Date(contract.startDate).getTime() <= time && (!end || time < end.getTime());
|
|
75
|
+
}) ?? null);
|
|
76
|
+
};
|
|
77
|
+
exports.getActiveContract = getActiveContract;
|
package/package.json
CHANGED
|
@@ -325,18 +325,19 @@ export class ContentMetricsHistoryEntry {
|
|
|
325
325
|
export const ContentMetricsHistoryEntrySchema = SchemaFactory.createForClass(ContentMetricsHistoryEntry);
|
|
326
326
|
|
|
327
327
|
/** Métricas estimadas pelo porte do perfil, para quando a rede não informa. Ver GD-751. */
|
|
328
|
+
// null é valor válido (sem estimativa): required recusaria null no create.
|
|
328
329
|
@Schema({ _id: false })
|
|
329
330
|
export class EstimatedMetrics {
|
|
330
|
-
@Prop({ type: Number, required:
|
|
331
|
+
@Prop({ type: Number, required: false, default: null })
|
|
331
332
|
impressionCount?: number | null;
|
|
332
333
|
|
|
333
|
-
@Prop({ type: Number, required:
|
|
334
|
+
@Prop({ type: Number, required: false, default: null })
|
|
334
335
|
reachCount?: number | null;
|
|
335
336
|
|
|
336
|
-
@Prop({ type: Number, required:
|
|
337
|
+
@Prop({ type: Number, required: false, default: null })
|
|
337
338
|
engagementCount?: number | null;
|
|
338
339
|
|
|
339
|
-
@Prop({ type: Number, required:
|
|
340
|
+
@Prop({ type: Number, required: false, default: null })
|
|
340
341
|
engagementRate?: number | null;
|
|
341
342
|
}
|
|
342
343
|
|
|
@@ -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);
|