@grapadigital/shared-schemas 1.0.219 → 1.0.220

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.
@@ -26,7 +26,7 @@
26
26
  import { HydratedDocument, Schema as MongooseSchema, Types } from 'mongoose';
27
27
  import { Recruitment } from './recruitment.schema';
28
28
  import { Content } from './content.schema';
29
- import { Client } from './client.schema';
29
+ import { Client, ClientMetricGoal } from './client.schema';
30
30
  import { Sale } from './sale.schema';
31
31
  import { Campaign } from './campaign.schema';
32
32
  import { Note } from './note.schema';
@@ -54,6 +54,34 @@ export declare const ActionPromptConfigSchema: MongooseSchema<ActionPromptConfig
54
54
  }>;
55
55
  export declare const ACTION_STATUS_VALUES: readonly ["config", "inProgress", "finished", "archived", "cancelled"];
56
56
  export type ActionStatus = (typeof ACTION_STATUS_VALUES)[number];
57
+ /**
58
+ * Fase da Ação (antigo briefing do genc). Com `_id` porque o Conteúdo aponta
59
+ * para ela (`Content.phase`). Período e hashtags opcionais fazem a captura
60
+ * sugerir a Fase sozinha; Fase sem os dois nunca é automática. Fases podem se
61
+ * sobrepor: vale a primeira por `order`. Ver GD-691.
62
+ */
63
+ export declare class ActionPhase {
64
+ _id: Types.ObjectId;
65
+ name: string;
66
+ /** Hex #rrggbb, da paleta do cpanel. */
67
+ color: string;
68
+ order: number;
69
+ /** Inclusivo. */
70
+ startDate?: Date | null;
71
+ /** Inclusivo (vale o dia inteiro). */
72
+ endDate?: Date | null;
73
+ /** Normalizadas (minúsculas, sem # e sem acento). */
74
+ hashtags: string[];
75
+ }
76
+ export declare const ActionPhaseSchema: MongooseSchema<ActionPhase, import("mongoose").Model<ActionPhase, any, any, any, import("mongoose").Document<unknown, any, ActionPhase, any, {}> & ActionPhase & Required<{
77
+ _id: Types.ObjectId;
78
+ }> & {
79
+ __v: number;
80
+ }, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ActionPhase, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ActionPhase>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ActionPhase> & Required<{
81
+ _id: Types.ObjectId;
82
+ }> & {
83
+ __v: number;
84
+ }>;
57
85
  export declare class Action {
58
86
  _id?: Types.ObjectId;
59
87
  title: string;
@@ -100,6 +128,15 @@ export declare class Action {
100
128
  businessUnit?: ActionBusinessUnit;
101
129
  campaign?: Types.ObjectId | Campaign;
102
130
  promptConfig?: ActionPromptConfig | null;
131
+ /**
132
+ * Hashtags que marcam o Conteúdo como Orgânico (Tipo, fora do escopo
133
+ * contratado). Normalizadas. Vazio = a captura não sugere Tipo. Ver GD-691.
134
+ */
135
+ organicHashtags?: string[];
136
+ /** Fases da Ação. Ver ActionPhase e GD-691. */
137
+ phases?: ActionPhase[];
138
+ /** Metas da Ação: mesmo { metric, value } e catálogo dos Tiers do cliente. Ver GD-691. */
139
+ goals?: ClientMetricGoal[];
103
140
  }
104
141
  export declare const ActionSchema: MongooseSchema<Action, import("mongoose").Model<Action, any, any, any, import("mongoose").Document<unknown, any, Action, any, {}> & Action & Required<{
105
142
  _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.ActionModel = exports.ActionSchema = exports.Action = exports.ACTION_STATUS_VALUES = exports.ActionPromptConfigSchema = exports.ActionPromptConfig = exports.ACTION_BUSINESS_UNIT_VALUES = void 0;
12
+ exports.ActionModel = exports.ActionSchema = exports.Action = exports.ActionPhaseSchema = exports.ActionPhase = exports.ACTION_STATUS_VALUES = exports.ActionPromptConfigSchema = exports.ActionPromptConfig = exports.ACTION_BUSINESS_UNIT_VALUES = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const swagger_1 = require("@nestjs/swagger");
15
15
  const mongoose_2 = require("mongoose");
@@ -75,6 +75,53 @@ exports.ACTION_STATUS_VALUES = [
75
75
  'archived',
76
76
  'cancelled',
77
77
  ];
78
+ /**
79
+ * Fase da Ação (antigo briefing do genc). Com `_id` porque o Conteúdo aponta
80
+ * para ela (`Content.phase`). Período e hashtags opcionais fazem a captura
81
+ * sugerir a Fase sozinha; Fase sem os dois nunca é automática. Fases podem se
82
+ * sobrepor: vale a primeira por `order`. Ver GD-691.
83
+ */
84
+ let ActionPhase = class ActionPhase {
85
+ };
86
+ __decorate([
87
+ (0, swagger_1.ApiProperty)({ example: '6710a1b2c3d4e5f601234567' }),
88
+ __metadata("design:type", mongoose_2.Types.ObjectId)
89
+ ], ActionPhase.prototype, "_id", void 0);
90
+ __decorate([
91
+ (0, swagger_1.ApiProperty)({ example: 'Lançamento' }),
92
+ (0, mongoose_1.Prop)({ type: String, required: true }),
93
+ __metadata("design:type", String)
94
+ ], ActionPhase.prototype, "name", void 0);
95
+ __decorate([
96
+ (0, swagger_1.ApiProperty)({ example: '#2563eb' }),
97
+ (0, mongoose_1.Prop)({ type: String, required: true }),
98
+ __metadata("design:type", String)
99
+ ], ActionPhase.prototype, "color", void 0);
100
+ __decorate([
101
+ (0, swagger_1.ApiProperty)({ example: 0, description: 'Prioridade quando Fases se sobrepõem (menor vale primeiro)' }),
102
+ (0, mongoose_1.Prop)({ type: Number, required: true }),
103
+ __metadata("design:type", Number)
104
+ ], ActionPhase.prototype, "order", void 0);
105
+ __decorate([
106
+ (0, swagger_1.ApiProperty)({ type: Date, required: false, nullable: true }),
107
+ (0, mongoose_1.Prop)({ type: Date, required: false, default: null }),
108
+ __metadata("design:type", Object)
109
+ ], ActionPhase.prototype, "startDate", void 0);
110
+ __decorate([
111
+ (0, swagger_1.ApiProperty)({ type: Date, required: false, nullable: true }),
112
+ (0, mongoose_1.Prop)({ type: Date, required: false, default: null }),
113
+ __metadata("design:type", Object)
114
+ ], ActionPhase.prototype, "endDate", void 0);
115
+ __decorate([
116
+ (0, swagger_1.ApiProperty)({ type: [String], example: ['galaxys26'] }),
117
+ (0, mongoose_1.Prop)({ type: [String], default: [] }),
118
+ __metadata("design:type", Array)
119
+ ], ActionPhase.prototype, "hashtags", void 0);
120
+ ActionPhase = __decorate([
121
+ (0, mongoose_1.Schema)()
122
+ ], ActionPhase);
123
+ exports.ActionPhase = ActionPhase;
124
+ exports.ActionPhaseSchema = mongoose_1.SchemaFactory.createForClass(ActionPhase);
78
125
  let Action = class Action {
79
126
  };
80
127
  __decorate([
@@ -297,6 +344,21 @@ __decorate([
297
344
  (0, mongoose_1.Prop)({ type: exports.ActionPromptConfigSchema, required: false, default: null }),
298
345
  __metadata("design:type", Object)
299
346
  ], Action.prototype, "promptConfig", void 0);
347
+ __decorate([
348
+ (0, swagger_1.ApiProperty)({ type: [String], required: false, example: ['galaxymoments'] }),
349
+ (0, mongoose_1.Prop)({ type: [String], default: [] }),
350
+ __metadata("design:type", Array)
351
+ ], Action.prototype, "organicHashtags", void 0);
352
+ __decorate([
353
+ (0, swagger_1.ApiProperty)({ type: () => [ActionPhase], required: false }),
354
+ (0, mongoose_1.Prop)({ type: [exports.ActionPhaseSchema], default: [] }),
355
+ __metadata("design:type", Array)
356
+ ], Action.prototype, "phases", void 0);
357
+ __decorate([
358
+ (0, swagger_1.ApiProperty)({ type: () => [client_schema_1.ClientMetricGoal], required: false }),
359
+ (0, mongoose_1.Prop)({ type: [client_schema_1.ClientMetricGoalSchema], default: [] }),
360
+ __metadata("design:type", Array)
361
+ ], Action.prototype, "goals", void 0);
300
362
  Action = __decorate([
301
363
  (0, mongoose_1.Schema)({ timestamps: true })
302
364
  ], Action);
@@ -40,7 +40,7 @@ export type ClientCustomization = (typeof CLIENT_CUSTOMIZATION_VALUES)[number];
40
40
  * `viralReachEngagementRate` é o limite de viral e só vale no Tier.
41
41
  * Ver GD-687.
42
42
  */
43
- export declare const CLIENT_GOAL_METRIC_VALUES: readonly ["potentialReach", "reachCount", "impressionCount", "engagementCount", "engagementRate", "reachEngagementRate", "qualifiedEngagementRate", "effectiveReach", "emv", "score", "cpmi", "cpi", "cpe", "viralReachEngagementRate"];
43
+ export declare const CLIENT_GOAL_METRIC_VALUES: readonly ["potentialReach", "reachCount", "impressionCount", "engagementCount", "engagementRate", "reachEngagementRate", "qualifiedEngagementRate", "effectiveReach", "emv", "score", "cpmi", "cpi", "cpe", "savedCount", "sharesCount", "vcr", "viralReachEngagementRate"];
44
44
  export type ClientGoalMetric = (typeof CLIENT_GOAL_METRIC_VALUES)[number];
45
45
  export type ClientGoalMetricUnit = 'count' | 'rate' | 'currency';
46
46
  /** Unidade de cada métrica. `rate` é guardado como fração (0,10 = 10%). */
@@ -40,6 +40,9 @@ exports.CLIENT_GOAL_METRIC_VALUES = [
40
40
  'cpmi',
41
41
  'cpi',
42
42
  'cpe',
43
+ 'savedCount',
44
+ 'sharesCount',
45
+ 'vcr',
43
46
  'viralReachEngagementRate',
44
47
  ];
45
48
  /** Unidade de cada métrica. `rate` é guardado como fração (0,10 = 10%). */
@@ -57,6 +60,9 @@ exports.CLIENT_GOAL_METRIC_UNITS = {
57
60
  cpmi: 'currency',
58
61
  cpi: 'currency',
59
62
  cpe: 'currency',
63
+ savedCount: 'count',
64
+ sharesCount: 'count',
65
+ vcr: 'rate',
60
66
  viralReachEngagementRate: 'rate',
61
67
  };
62
68
  let Cluster = class Cluster {
@@ -111,10 +117,12 @@ exports.CommissionRangeSchema = mongoose_1.SchemaFactory.createForClass(Commissi
111
117
  let ClientMetricGoal = class ClientMetricGoal {
112
118
  };
113
119
  __decorate([
120
+ (0, swagger_1.ApiProperty)({ enum: exports.CLIENT_GOAL_METRIC_VALUES, example: 'potentialReach' }),
114
121
  (0, mongoose_1.Prop)({ type: String, enum: exports.CLIENT_GOAL_METRIC_VALUES, required: true }),
115
122
  __metadata("design:type", String)
116
123
  ], ClientMetricGoal.prototype, "metric", void 0);
117
124
  __decorate([
125
+ (0, swagger_1.ApiProperty)({ example: 120000000, description: 'Na unidade da métrica; percentuais em fração (0,25 = 25%)' }),
118
126
  (0, mongoose_1.Prop)({ type: Number, required: true }),
119
127
  __metadata("design:type", Number)
120
128
  ], ClientMetricGoal.prototype, "value", void 0);
@@ -157,6 +157,9 @@ export declare const CONTENT_REPORTABLE_STATUS_VALUES: ContentStatus[];
157
157
  export declare const CONTENT_COMMENT_COUNT_SOURCE_VALUES: readonly ["primetag", "exportcomments"];
158
158
  export type ContentCommentCountSource = (typeof CONTENT_COMMENT_COUNT_SOURCE_VALUES)[number];
159
159
  export declare const CONTENT_METRICS_SOURCE_VALUES: readonly ["real", "organic", "estimated"];
160
+ /** Origem do Tipo e da Fase: auto = regra da captura, manual = quem revisou. Ver GD-691. */
161
+ export declare const CONTENT_CLASSIFICATION_SOURCE_VALUES: readonly ["auto", "manual"];
162
+ export type ContentClassificationSource = (typeof CONTENT_CLASSIFICATION_SOURCE_VALUES)[number];
160
163
  export type ContentMetricsSource = (typeof CONTENT_METRICS_SOURCE_VALUES)[number];
161
164
  export declare class ContentStatusChangedBy {
162
165
  team?: string;
@@ -207,6 +210,18 @@ export declare class Content {
207
210
  editedAt?: Date;
208
211
  followersAtPost?: number | null;
209
212
  history?: ContentMetricsHistoryEntry[];
213
+ /** Hashtags da publicação, normalizadas (minúsculas, sem # e sem acento). Ver GD-691. */
214
+ hashtags?: string[];
215
+ /**
216
+ * Tipo do Conteúdo: Orgânico (fora do escopo contratado) ou Pago. null =
217
+ * Ação sem hashtag de Orgânico. Não confundir com metricsSource 'organic'
218
+ * (métrica pública) nem com isBoosted (mídia paga). Ver GD-691.
219
+ */
220
+ isOrganic?: boolean | null;
221
+ isOrganicSource?: ContentClassificationSource | null;
222
+ /** _id de uma Action.phases. Ver GD-691. */
223
+ phase?: Types.ObjectId | null;
224
+ phaseSource?: ContentClassificationSource | null;
210
225
  }
211
226
  export declare const ContentSchema: MongooseSchema<Content, import("mongoose").Model<Content, any, any, any, import("mongoose").Document<unknown, any, Content, any, {}> & Content & Required<{
212
227
  _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.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = 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.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
12
+ exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_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.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
13
13
  const mongoose_1 = require("@nestjs/mongoose");
14
14
  const mongoose_2 = require("mongoose");
15
15
  let SentimentComment = class SentimentComment {
@@ -273,6 +273,8 @@ exports.CONTENT_REPORTABLE_STATUS_VALUES = [
273
273
  ];
274
274
  exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = ['primetag', 'exportcomments'];
275
275
  exports.CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'];
276
+ /** Origem do Tipo e da Fase: auto = regra da captura, manual = quem revisou. Ver GD-691. */
277
+ exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = ['auto', 'manual'];
276
278
  let ContentStatusChangedBy = class ContentStatusChangedBy {
277
279
  };
278
280
  __decorate([
@@ -449,6 +451,26 @@ __decorate([
449
451
  (0, mongoose_1.Prop)({ type: [exports.ContentMetricsHistoryEntrySchema], required: false, default: [] }),
450
452
  __metadata("design:type", Array)
451
453
  ], Content.prototype, "history", void 0);
454
+ __decorate([
455
+ (0, mongoose_1.Prop)({ type: [String], default: [] }),
456
+ __metadata("design:type", Array)
457
+ ], Content.prototype, "hashtags", void 0);
458
+ __decorate([
459
+ (0, mongoose_1.Prop)({ type: Boolean, required: false, default: null }),
460
+ __metadata("design:type", Object)
461
+ ], Content.prototype, "isOrganic", void 0);
462
+ __decorate([
463
+ (0, mongoose_1.Prop)({ type: String, enum: exports.CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null }),
464
+ __metadata("design:type", Object)
465
+ ], Content.prototype, "isOrganicSource", void 0);
466
+ __decorate([
467
+ (0, mongoose_1.Prop)({ type: mongoose_2.Schema.Types.ObjectId, required: false, default: null }),
468
+ __metadata("design:type", Object)
469
+ ], Content.prototype, "phase", void 0);
470
+ __decorate([
471
+ (0, mongoose_1.Prop)({ type: String, enum: exports.CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null }),
472
+ __metadata("design:type", Object)
473
+ ], Content.prototype, "phaseSource", void 0);
452
474
  Content = __decorate([
453
475
  (0, mongoose_1.Schema)({ timestamps: true })
454
476
  ], Content);
@@ -459,6 +481,8 @@ exports.ContentSchema.index({ action: 1, profile: 1, type: 1 });
459
481
  exports.ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
460
482
  exports.ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
461
483
  exports.ContentSchema.index({ profile: 1 });
484
+ exports.ContentSchema.index({ action: 1, phase: 1 });
485
+ exports.ContentSchema.index({ action: 1, isOrganic: 1 });
462
486
  exports.ContentSchema.index({ createdAt: -1, status: 1 });
463
487
  // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
464
488
  // primetagId) ficam fora do índice.
@@ -0,0 +1,112 @@
1
+ # Spec: action + content: Orgânico, Fases e Metas da Ação
2
+
3
+ **Feature:** GD-692 (Story GD-691)
4
+ **Consumido por:** api-campaigns (GD-693), content-capture-trigger e content-capture-worker (GD-693), web-campaigns (GD-694, via shared-app-modules)
5
+
6
+ ---
7
+
8
+ ## Descrição
9
+
10
+ A Ação da Samsung passa a guardar a hashtag de Orgânico, as Fases (antigo briefing do genc) e as
11
+ Metas da Ação. O Conteúdo passa a guardar as próprias hashtags e o Tipo (Orgânico ou Pago) e a Fase
12
+ sugeridos, cada um com a origem (`auto` = regra da captura, `manual` = quem revisou).
13
+
14
+ Todos os campos são opcionais e têm default vazio: Ações e Conteúdos existentes não mudam.
15
+
16
+ **Nome:** "Orgânico" aqui é o Tipo do Conteúdo (fora do escopo contratado). Não confundir com
17
+ `metricsSource: 'organic'` (Métrica pública) nem com `isBoosted` (mídia paga, GD-707).
18
+
19
+ ## Schema
20
+
21
+ ### `action.schema.ts`
22
+
23
+ ```ts
24
+ @Schema() // com _id: o Conteúdo aponta para a Fase
25
+ export class ActionPhase {
26
+ _id: Types.ObjectId;
27
+ name: string;
28
+ color: string; // hex #rrggbb (paleta do cpanel)
29
+ order: number; // 0..n, define a prioridade quando Fases se sobrepõem
30
+ startDate?: Date | null; // inclusivo
31
+ endDate?: Date | null; // inclusivo (dia inteiro)
32
+ hashtags: string[]; // normalizadas, default []
33
+ }
34
+
35
+ Action.organicHashtags: string[] // normalizadas, default []
36
+ Action.phases: ActionPhase[] // default []
37
+ Action.goals: ClientMetricGoal[] // default []; mesmo { metric, value } e catálogo dos Tiers (GD-688)
38
+ ```
39
+
40
+ ### `content.schema.ts`
41
+
42
+ ```ts
43
+ export const CONTENT_CLASSIFICATION_SOURCE_VALUES = ['auto', 'manual'] as const;
44
+
45
+ Content.hashtags: string[] // normalizadas, default []
46
+ Content.isOrganic: boolean | null // default null (Ação sem hashtag de Orgânico)
47
+ Content.isOrganicSource: 'auto' | 'manual' | null // default null
48
+ Content.phase: Types.ObjectId | null // _id de Action.phases; default null
49
+ Content.phaseSource: 'auto' | 'manual' | null // default null
50
+ ```
51
+
52
+ Índices novos: `ContentSchema.index({ action: 1, phase: 1 })` e `ContentSchema.index({ action: 1, isOrganic: 1 })`.
53
+
54
+ ### Catálogo de métricas (`client.schema.ts`)
55
+
56
+ `CLIENT_GOAL_METRIC_VALUES` ganha as métricas do relatório Samsung que faltavam, com a unidade em
57
+ `CLIENT_GOAL_METRIC_UNITS`:
58
+
59
+ | métrica | unidade |
60
+ |---|---|
61
+ | `savedCount` | count |
62
+ | `sharesCount` | count |
63
+ | `vcr` | rate (a métrica entra no Content na GD-707) |
64
+
65
+ O mesmo vale no espelho do shared-app-modules (`CLIENT_GOAL_METRIC_VALUES` com label): "Salvamentos",
66
+ "Compartilhamentos", "VCR".
67
+
68
+ ## Normalização de hashtag
69
+
70
+ Regra única, implementada **localmente em cada repo que usa** (decisão da Story): api-campaigns,
71
+ content-capture-worker e web-campaigns. Não vai para o shared-schemas.
72
+
73
+ `normalizeHashtag(value)`: tira espaços e `#` do começo, remove acentos (NFD), passa para
74
+ minúsculas e remove o que não for `[a-z0-9_]`.
75
+
76
+ - `"#GalaxyMoments"` → `"galaxymoments"`
77
+ - `"#VerãoSamsung"` → `"veraosamsung"`
78
+
79
+ ---
80
+
81
+ ## Arquivos
82
+
83
+ - Alterar `src/schemas/action.schema.ts` (ActionPhase + 3 campos).
84
+ - Alterar `src/schemas/content.schema.ts` (5 campos, enum de origem, 2 índices).
85
+ - Alterar `src/schemas/client.schema.ts` (3 métricas no catálogo).
86
+ - Alterar `package.json` (1.0.220).
87
+ - shared-app-modules: `src/app/constants/values.constant.ts` (3 métricas com label) e
88
+ `src/app/interfaces/action/action.interface.ts` (`organicHashtags`, `phases`, `goals`, mais
89
+ `ActionPhaseInterface`); interface de Content usada no web-campaigns, se houver no pacote.
90
+
91
+ ## Passos
92
+
93
+ 1. `ActionPhase` com `_id` (subdocumento com id) e os três campos novos na Action, com JSDoc em PT e "Ver GD-691".
94
+ 2. Campos e índices do Content.
95
+ 3. Métricas novas no catálogo do client.
96
+ 4. `npm run build` e conferir no `dist` os campos e exports; checar com `validateSync` que uma Action e um Content antigos (sem os campos) continuam válidos e que `phases[0]._id` é gerado.
97
+ 5. shared-app-modules: catálogo e interfaces; `pnpm run build`.
98
+ 6. Publicar os dois pacotes (José).
99
+
100
+ ## Dependências
101
+
102
+ - GD-688 (catálogo de métricas) já em produção.
103
+ - Esta Task vem antes da GD-693 e da GD-694.
104
+
105
+ ## Validação
106
+
107
+ - `npm run build` no shared-schemas; `pnpm run build` no shared-app-modules.
108
+ - `validateSync` de documentos antigos e novos (script rápido com o `dist`).
109
+
110
+ ## Pontos abertos
111
+
112
+ - `vcr` entra no catálogo antes de existir no Content (chega na GD-707). Até lá a meta pode ser cadastrada, mas o Hub mostra o realizado como indisponível.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grapadigital/shared-schemas",
3
- "version": "1.0.219",
3
+ "version": "1.0.220",
4
4
  "description": "Shared Mongoose Schemas",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  import { Recruitment } from './recruitment.schema';
12
12
  import { Content } from './content.schema';
13
- import { Client } from './client.schema';
13
+ import { Client, ClientMetricGoal, ClientMetricGoalSchema } from './client.schema';
14
14
  import { Sale } from './sale.schema';
15
15
  import { Campaign } from './campaign.schema';
16
16
  import { Note, NoteSchema } from './note.schema';
@@ -68,6 +68,48 @@ export const ACTION_STATUS_VALUES = [
68
68
  ] as const;
69
69
  export type ActionStatus = (typeof ACTION_STATUS_VALUES)[number];
70
70
 
71
+ /**
72
+ * Fase da Ação (antigo briefing do genc). Com `_id` porque o Conteúdo aponta
73
+ * para ela (`Content.phase`). Período e hashtags opcionais fazem a captura
74
+ * sugerir a Fase sozinha; Fase sem os dois nunca é automática. Fases podem se
75
+ * sobrepor: vale a primeira por `order`. Ver GD-691.
76
+ */
77
+ @Schema()
78
+ export class ActionPhase {
79
+ @ApiProperty({ example: '6710a1b2c3d4e5f601234567' })
80
+ _id: Types.ObjectId;
81
+
82
+ @ApiProperty({ example: 'Lançamento' })
83
+ @Prop({ type: String, required: true })
84
+ name: string;
85
+
86
+ /** Hex #rrggbb, da paleta do cpanel. */
87
+ @ApiProperty({ example: '#2563eb' })
88
+ @Prop({ type: String, required: true })
89
+ color: string;
90
+
91
+ @ApiProperty({ example: 0, description: 'Prioridade quando Fases se sobrepõem (menor vale primeiro)' })
92
+ @Prop({ type: Number, required: true })
93
+ order: number;
94
+
95
+ /** Inclusivo. */
96
+ @ApiProperty({ type: Date, required: false, nullable: true })
97
+ @Prop({ type: Date, required: false, default: null })
98
+ startDate?: Date | null;
99
+
100
+ /** Inclusivo (vale o dia inteiro). */
101
+ @ApiProperty({ type: Date, required: false, nullable: true })
102
+ @Prop({ type: Date, required: false, default: null })
103
+ endDate?: Date | null;
104
+
105
+ /** Normalizadas (minúsculas, sem # e sem acento). */
106
+ @ApiProperty({ type: [String], example: ['galaxys26'] })
107
+ @Prop({ type: [String], default: [] })
108
+ hashtags: string[];
109
+ }
110
+
111
+ export const ActionPhaseSchema = SchemaFactory.createForClass(ActionPhase);
112
+
71
113
  @Schema({ timestamps: true })
72
114
  export class Action {
73
115
  @ApiProperty({ example: '64bfc3a78d3f1e2a12345678' })
@@ -252,6 +294,24 @@ export class Action {
252
294
  @ApiProperty({ type: () => ActionPromptConfig, required: false, description: 'Customização de prompts de classificação de comentário por IA para esta action — campos ausentes caem no prompt genérico default' })
253
295
  @Prop({ type: ActionPromptConfigSchema, required: false, default: null })
254
296
  promptConfig?: ActionPromptConfig | null;
297
+
298
+ /**
299
+ * Hashtags que marcam o Conteúdo como Orgânico (Tipo, fora do escopo
300
+ * contratado). Normalizadas. Vazio = a captura não sugere Tipo. Ver GD-691.
301
+ */
302
+ @ApiProperty({ type: [String], required: false, example: ['galaxymoments'] })
303
+ @Prop({ type: [String], default: [] })
304
+ organicHashtags?: string[];
305
+
306
+ /** Fases da Ação. Ver ActionPhase e GD-691. */
307
+ @ApiProperty({ type: () => [ActionPhase], required: false })
308
+ @Prop({ type: [ActionPhaseSchema], default: [] })
309
+ phases?: ActionPhase[];
310
+
311
+ /** Metas da Ação: mesmo { metric, value } e catálogo dos Tiers do cliente. Ver GD-691. */
312
+ @ApiProperty({ type: () => [ClientMetricGoal], required: false })
313
+ @Prop({ type: [ClientMetricGoalSchema], default: [] })
314
+ goals?: ClientMetricGoal[];
255
315
  }
256
316
 
257
317
  export const ActionSchema = SchemaFactory.createForClass(Action);
@@ -39,6 +39,9 @@ export const CLIENT_GOAL_METRIC_VALUES = [
39
39
  'cpmi',
40
40
  'cpi',
41
41
  'cpe',
42
+ 'savedCount',
43
+ 'sharesCount',
44
+ 'vcr',
42
45
  'viralReachEngagementRate',
43
46
  ] as const;
44
47
  export type ClientGoalMetric = (typeof CLIENT_GOAL_METRIC_VALUES)[number];
@@ -60,6 +63,9 @@ export const CLIENT_GOAL_METRIC_UNITS: Record<ClientGoalMetric, ClientGoalMetric
60
63
  cpmi: 'currency',
61
64
  cpi: 'currency',
62
65
  cpe: 'currency',
66
+ savedCount: 'count',
67
+ sharesCount: 'count',
68
+ vcr: 'rate',
63
69
  viralReachEngagementRate: 'rate',
64
70
  };
65
71
 
@@ -101,10 +107,12 @@ export const CommissionRangeSchema = SchemaFactory.createForClass(CommissionRang
101
107
 
102
108
  @Schema({ _id: false })
103
109
  export class ClientMetricGoal {
110
+ @ApiProperty({ enum: CLIENT_GOAL_METRIC_VALUES, example: 'potentialReach' })
104
111
  @Prop({ type: String, enum: CLIENT_GOAL_METRIC_VALUES, required: true })
105
112
  metric: ClientGoalMetric;
106
113
 
107
114
  /** Na unidade da métrica (`CLIENT_GOAL_METRIC_UNITS`); `rate` em fração. */
115
+ @ApiProperty({ example: 120000000, description: 'Na unidade da métrica; percentuais em fração (0,25 = 25%)' })
108
116
  @Prop({ type: Number, required: true })
109
117
  value: number;
110
118
  }
@@ -217,6 +217,10 @@ export const CONTENT_COMMENT_COUNT_SOURCE_VALUES = ['primetag', 'exportcomments'
217
217
  export type ContentCommentCountSource = (typeof CONTENT_COMMENT_COUNT_SOURCE_VALUES)[number];
218
218
 
219
219
  export const CONTENT_METRICS_SOURCE_VALUES = ['real', 'organic', 'estimated'] as const;
220
+
221
+ /** Origem do Tipo e da Fase: auto = regra da captura, manual = quem revisou. Ver GD-691. */
222
+ export const CONTENT_CLASSIFICATION_SOURCE_VALUES = ['auto', 'manual'] as const;
223
+ export type ContentClassificationSource = (typeof CONTENT_CLASSIFICATION_SOURCE_VALUES)[number];
220
224
  export type ContentMetricsSource = (typeof CONTENT_METRICS_SOURCE_VALUES)[number];
221
225
 
222
226
  @Schema({ _id: false })
@@ -378,6 +382,28 @@ export class Content {
378
382
  // empurra o `metrics` anterior (antes de ser sobrescrito) pra cá.
379
383
  @Prop({ type: [ContentMetricsHistoryEntrySchema], required: false, default: [] })
380
384
  history?: ContentMetricsHistoryEntry[];
385
+
386
+ /** Hashtags da publicação, normalizadas (minúsculas, sem # e sem acento). Ver GD-691. */
387
+ @Prop({ type: [String], default: [] })
388
+ hashtags?: string[];
389
+
390
+ /**
391
+ * Tipo do Conteúdo: Orgânico (fora do escopo contratado) ou Pago. null =
392
+ * Ação sem hashtag de Orgânico. Não confundir com metricsSource 'organic'
393
+ * (métrica pública) nem com isBoosted (mídia paga). Ver GD-691.
394
+ */
395
+ @Prop({ type: Boolean, required: false, default: null })
396
+ isOrganic?: boolean | null;
397
+
398
+ @Prop({ type: String, enum: CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null })
399
+ isOrganicSource?: ContentClassificationSource | null;
400
+
401
+ /** _id de uma Action.phases. Ver GD-691. */
402
+ @Prop({ type: MongooseSchema.Types.ObjectId, required: false, default: null })
403
+ phase?: Types.ObjectId | null;
404
+
405
+ @Prop({ type: String, enum: CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null })
406
+ phaseSource?: ContentClassificationSource | null;
381
407
  }
382
408
 
383
409
  export const ContentSchema = SchemaFactory.createForClass(Content);
@@ -387,6 +413,8 @@ ContentSchema.index({ action: 1, profile: 1, type: 1 });
387
413
  ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
388
414
  ContentSchema.index({ action: 1, status: 1, postedAt: 1 });
389
415
  ContentSchema.index({ profile: 1 });
416
+ ContentSchema.index({ action: 1, phase: 1 });
417
+ ContentSchema.index({ action: 1, isOrganic: 1 });
390
418
  ContentSchema.index({ createdAt: -1, status: 1 });
391
419
  // Chave de idempotência da captura da Primetag — conteúdos manuais (sem
392
420
  // primetagId) ficam fora do índice.