evo360-types 1.3.469 → 1.3.472
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/apps/evo-campaigns/zod-schemas.d.ts +15 -4
- package/dist/apps/evo-campaigns/zod-schemas.js +14 -0
- package/dist/apps/evo-campaigns/zod-schemas.ts +14 -0
- package/dist/types/evo-campaigns/index.d.ts +103 -4
- package/dist/types/evo-campaigns/index.ts +112 -4
- package/dist/types/evo-finops/common/contract.d.ts +17 -1
- package/dist/types/evo-finops/common/contract.js +14 -2
- package/dist/types/evo-finops/common/contract.ts +21 -2
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ export type ICampaignStatus = z.infer<typeof zCampaignStatusSchema>;
|
|
|
4
4
|
export declare const zCampaignTypeSchema: z.ZodEnum<["conversion", "informative", "relationship", "operational", "promotional"]>;
|
|
5
5
|
export declare const zCampaignChannelSchema: z.ZodEnum<["whatsapp", "email"]>;
|
|
6
6
|
export declare const zCampaignVariableSourceSchema: z.ZodEnum<["placeholder", "fixed", "csv_column", "qr_link"]>;
|
|
7
|
-
|
|
7
|
+
/** ADDITIVE ONLY. The FE parses every recipient through this enum; a value written
|
|
8
|
+
* to Firestore but missing here makes it drop the WHOLE recipient document. */
|
|
9
|
+
export declare const zRecipientDispatchStateSchema: z.ZodEnum<["pending", "dispatched", "done", "excluded", "canceled"]>;
|
|
8
10
|
export declare const zRecipientChannelStatusSchema: z.ZodEnum<["pending", "scheduled", "sent", "delivered", "read", "failed", "clicked", "replied"]>;
|
|
9
11
|
export declare const zRecipientInteractionStatusSchema: z.ZodEnum<["pendente", "inelegivel", "agendado", "enviado", "entregue", "lido", "clicou", "respondeu", "convertido", "falhou", "optout", "cancelado"]>;
|
|
10
12
|
export declare const zCampaignCodeNameSchema: z.ZodObject<{
|
|
@@ -3738,6 +3740,7 @@ export declare const zCampaignSchema: z.ZodObject<{
|
|
|
3738
3740
|
prevalidation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3739
3741
|
cost_estimate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3740
3742
|
counters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3743
|
+
last_materialize: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
3741
3744
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
3742
3745
|
id: z.ZodString;
|
|
3743
3746
|
ref: z.ZodAny;
|
|
@@ -6726,6 +6729,7 @@ export declare const zCampaignSchema: z.ZodObject<{
|
|
|
6726
6729
|
prevalidation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6727
6730
|
cost_estimate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6728
6731
|
counters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6732
|
+
last_materialize: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
6729
6733
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
6730
6734
|
id: z.ZodString;
|
|
6731
6735
|
ref: z.ZodAny;
|
|
@@ -9714,6 +9718,7 @@ export declare const zCampaignSchema: z.ZodObject<{
|
|
|
9714
9718
|
prevalidation: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9715
9719
|
cost_estimate: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9716
9720
|
counters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9721
|
+
last_materialize: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9717
9722
|
}, z.ZodTypeAny, "passthrough">>;
|
|
9718
9723
|
export declare const zCampaignBucketSourceTypeSchema: z.ZodEnum<["leads", "patients", "appointments", "csv"]>;
|
|
9719
9724
|
export declare const zCampaignBucketSchema: z.ZodObject<{
|
|
@@ -9790,7 +9795,9 @@ export declare const zCampaignRecipientSchema: z.ZodObject<{
|
|
|
9790
9795
|
bucket_ids: z.ZodArray<z.ZodString, "many">;
|
|
9791
9796
|
primary_bucket_id: z.ZodString;
|
|
9792
9797
|
csv_variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9793
|
-
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded"]>;
|
|
9798
|
+
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded", "canceled"]>;
|
|
9799
|
+
/** Absent (field-deleted), not false, once a manual exclusion is restored. */
|
|
9800
|
+
manual_excluded: z.ZodOptional<z.ZodBoolean>;
|
|
9794
9801
|
eligibility: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9795
9802
|
channels: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9796
9803
|
interaction_status: z.ZodEnum<["pendente", "inelegivel", "agendado", "enviado", "entregue", "lido", "clicou", "respondeu", "convertido", "falhou", "optout", "cancelado"]>;
|
|
@@ -9827,7 +9834,9 @@ export declare const zCampaignRecipientSchema: z.ZodObject<{
|
|
|
9827
9834
|
bucket_ids: z.ZodArray<z.ZodString, "many">;
|
|
9828
9835
|
primary_bucket_id: z.ZodString;
|
|
9829
9836
|
csv_variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9830
|
-
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded"]>;
|
|
9837
|
+
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded", "canceled"]>;
|
|
9838
|
+
/** Absent (field-deleted), not false, once a manual exclusion is restored. */
|
|
9839
|
+
manual_excluded: z.ZodOptional<z.ZodBoolean>;
|
|
9831
9840
|
eligibility: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9832
9841
|
channels: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9833
9842
|
interaction_status: z.ZodEnum<["pendente", "inelegivel", "agendado", "enviado", "entregue", "lido", "clicou", "respondeu", "convertido", "falhou", "optout", "cancelado"]>;
|
|
@@ -9864,7 +9873,9 @@ export declare const zCampaignRecipientSchema: z.ZodObject<{
|
|
|
9864
9873
|
bucket_ids: z.ZodArray<z.ZodString, "many">;
|
|
9865
9874
|
primary_bucket_id: z.ZodString;
|
|
9866
9875
|
csv_variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9867
|
-
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded"]>;
|
|
9876
|
+
dispatch_state: z.ZodEnum<["pending", "dispatched", "done", "excluded", "canceled"]>;
|
|
9877
|
+
/** Absent (field-deleted), not false, once a manual exclusion is restored. */
|
|
9878
|
+
manual_excluded: z.ZodOptional<z.ZodBoolean>;
|
|
9868
9879
|
eligibility: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9869
9880
|
channels: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
9870
9881
|
interaction_status: z.ZodEnum<["pendente", "inelegivel", "agendado", "enviado", "entregue", "lido", "clicou", "respondeu", "convertido", "falhou", "optout", "cancelado"]>;
|
|
@@ -34,11 +34,16 @@ exports.zCampaignVariableSourceSchema = zod_1.z.enum([
|
|
|
34
34
|
"csv_column",
|
|
35
35
|
"qr_link",
|
|
36
36
|
]);
|
|
37
|
+
/** ADDITIVE ONLY. The FE parses every recipient through this enum; a value written
|
|
38
|
+
* to Firestore but missing here makes it drop the WHOLE recipient document. */
|
|
37
39
|
exports.zRecipientDispatchStateSchema = zod_1.z.enum([
|
|
38
40
|
"pending",
|
|
39
41
|
"dispatched",
|
|
40
42
|
"done",
|
|
41
43
|
"excluded",
|
|
44
|
+
// Declared in RecipientDispatchState but with no writer yet — listed here FIRST so
|
|
45
|
+
// whoever starts writing it doesn't take the FE's recipient list down with them.
|
|
46
|
+
"canceled",
|
|
42
47
|
]);
|
|
43
48
|
exports.zRecipientChannelStatusSchema = zod_1.z.enum([
|
|
44
49
|
"pending",
|
|
@@ -194,10 +199,15 @@ exports.zCampaignSchema = zod_schemas_1.zFireDocSchema
|
|
|
194
199
|
new_lead_config: exports.zCampaignNewLeadConfigSchema.optional(),
|
|
195
200
|
frequency: exports.zCampaignFrequencySchema,
|
|
196
201
|
schedule: exports.zCampaignScheduleSchema,
|
|
202
|
+
// Backend-owned state maps: declared so the field is documented, deliberately
|
|
203
|
+
// left unvalidated. They gain fields slice by slice and the FE reads them
|
|
204
|
+
// defensively — a structural schema here would drop the whole campaign doc the
|
|
205
|
+
// first time the backend writes a counter the FE's evo-types doesn't know yet.
|
|
197
206
|
dispatch_state: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
198
207
|
prevalidation: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
199
208
|
cost_estimate: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
200
209
|
counters: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
210
|
+
last_materialize: zod_1.z.record(zod_1.z.unknown()).optional(),
|
|
201
211
|
})
|
|
202
212
|
.passthrough();
|
|
203
213
|
// ----- Bucket doc -----------------------------------------------------------
|
|
@@ -234,6 +244,10 @@ exports.zCampaignRecipientSchema = zod_schemas_1.zFireDocSchema
|
|
|
234
244
|
primary_bucket_id: zod_1.z.string(),
|
|
235
245
|
csv_variables: zod_1.z.record(zod_1.z.string()).optional(),
|
|
236
246
|
dispatch_state: exports.zRecipientDispatchStateSchema,
|
|
247
|
+
/** Absent (field-deleted), not false, once a manual exclusion is restored. */
|
|
248
|
+
manual_excluded: zod_1.z.boolean().optional(),
|
|
249
|
+
// Same reasoning as the campaign state maps: unvalidated on purpose, so a new
|
|
250
|
+
// exclusion reason or channel field never costs the FE the whole recipient row.
|
|
237
251
|
eligibility: zod_1.z.record(zod_1.z.unknown()),
|
|
238
252
|
channels: zod_1.z.record(zod_1.z.unknown()),
|
|
239
253
|
interaction_status: exports.zRecipientInteractionStatusSchema,
|
|
@@ -38,11 +38,16 @@ export const zCampaignVariableSourceSchema = z.enum([
|
|
|
38
38
|
"qr_link",
|
|
39
39
|
]);
|
|
40
40
|
|
|
41
|
+
/** ADDITIVE ONLY. The FE parses every recipient through this enum; a value written
|
|
42
|
+
* to Firestore but missing here makes it drop the WHOLE recipient document. */
|
|
41
43
|
export const zRecipientDispatchStateSchema = z.enum([
|
|
42
44
|
"pending",
|
|
43
45
|
"dispatched",
|
|
44
46
|
"done",
|
|
45
47
|
"excluded",
|
|
48
|
+
// Declared in RecipientDispatchState but with no writer yet — listed here FIRST so
|
|
49
|
+
// whoever starts writing it doesn't take the FE's recipient list down with them.
|
|
50
|
+
"canceled",
|
|
46
51
|
]);
|
|
47
52
|
|
|
48
53
|
export const zRecipientChannelStatusSchema = z.enum([
|
|
@@ -212,10 +217,15 @@ export const zCampaignSchema = zFireDocSchema
|
|
|
212
217
|
new_lead_config: zCampaignNewLeadConfigSchema.optional(),
|
|
213
218
|
frequency: zCampaignFrequencySchema,
|
|
214
219
|
schedule: zCampaignScheduleSchema,
|
|
220
|
+
// Backend-owned state maps: declared so the field is documented, deliberately
|
|
221
|
+
// left unvalidated. They gain fields slice by slice and the FE reads them
|
|
222
|
+
// defensively — a structural schema here would drop the whole campaign doc the
|
|
223
|
+
// first time the backend writes a counter the FE's evo-types doesn't know yet.
|
|
215
224
|
dispatch_state: z.record(z.unknown()).optional(),
|
|
216
225
|
prevalidation: z.record(z.unknown()).optional(),
|
|
217
226
|
cost_estimate: z.record(z.unknown()).optional(),
|
|
218
227
|
counters: z.record(z.unknown()).optional(),
|
|
228
|
+
last_materialize: z.record(z.unknown()).optional(),
|
|
219
229
|
})
|
|
220
230
|
.passthrough();
|
|
221
231
|
|
|
@@ -255,6 +265,10 @@ export const zCampaignRecipientSchema = zFireDocSchema
|
|
|
255
265
|
primary_bucket_id: z.string(),
|
|
256
266
|
csv_variables: z.record(z.string()).optional(),
|
|
257
267
|
dispatch_state: zRecipientDispatchStateSchema,
|
|
268
|
+
/** Absent (field-deleted), not false, once a manual exclusion is restored. */
|
|
269
|
+
manual_excluded: z.boolean().optional(),
|
|
270
|
+
// Same reasoning as the campaign state maps: unvalidated on purpose, so a new
|
|
271
|
+
// exclusion reason or channel field never costs the FE the whole recipient row.
|
|
258
272
|
eligibility: z.record(z.unknown()),
|
|
259
273
|
channels: z.record(z.unknown()),
|
|
260
274
|
interaction_status: zRecipientInteractionStatusSchema,
|
|
@@ -181,16 +181,61 @@ export interface ICampaignCounters {
|
|
|
181
181
|
unique?: number;
|
|
182
182
|
eligible?: number;
|
|
183
183
|
excluded_optout?: number;
|
|
184
|
+
/** Excluded by the 15-day fatigue rule. INCLUDES `excluded_frequency_unavailable`
|
|
185
|
+
* (the fail-closed subset) — never sum the two, the second is a breakdown of
|
|
186
|
+
* the first. */
|
|
184
187
|
excluded_frequency?: number;
|
|
188
|
+
/**
|
|
189
|
+
* Subset of `excluded_frequency` excluded because the lead's marketing history
|
|
190
|
+
* could NOT be read, so the check failed CLOSED — not because the contact was
|
|
191
|
+
* touched recently. Reported apart because the two lead to opposite actions:
|
|
192
|
+
* "everybody was contacted in the last 15 days" (wait) vs "the history query is
|
|
193
|
+
* broken" (fix the index and re-materialize). Rolling them together made the
|
|
194
|
+
* screen state something actively false (F3.1 review A1).
|
|
195
|
+
*/
|
|
196
|
+
excluded_frequency_unavailable?: number;
|
|
197
|
+
/** Always 0 today — no code path increments it (see audit note at the bottom). */
|
|
185
198
|
excluded_invalid?: number;
|
|
186
199
|
excluded_no_channel?: number;
|
|
187
200
|
excluded_duplicate?: number;
|
|
201
|
+
/** Recipients removed by hand from the audience (`manual_excluded=true`). Written
|
|
202
|
+
* wholesale by the materializer and incremented/decremented by the exclude/restore
|
|
203
|
+
* endpoints. */
|
|
204
|
+
excluded_manual?: number;
|
|
188
205
|
per_channel?: {
|
|
189
206
|
whatsapp?: ICampaignChannelCounters;
|
|
190
207
|
email?: ICampaignChannelCounters;
|
|
191
208
|
};
|
|
192
209
|
converted?: number;
|
|
193
210
|
}
|
|
211
|
+
/** Caveat on a materialization that SUCCEEDED but whose result is not what it looks like. */
|
|
212
|
+
export type CampaignMaterializeWarning =
|
|
213
|
+
/** The campaign has no bucket at all — the audience is empty by construction,
|
|
214
|
+
* not because the filters matched nobody. */
|
|
215
|
+
"no_buckets"
|
|
216
|
+
/** The 15-day fatigue check could not read the lead history and failed closed:
|
|
217
|
+
* the recipients were excluded as a precaution, not for being contacted recently. */
|
|
218
|
+
| "frequency_check_unavailable";
|
|
219
|
+
/**
|
|
220
|
+
* Progress/outcome stamp of the last audience materialization, written by the
|
|
221
|
+
* `campaign-materialize` handler (deep merge, so `error`/`warning` are written
|
|
222
|
+
* explicitly as null when absent — a stale caveat must not survive the next run).
|
|
223
|
+
* Drives the FE progress indicator and gates the interactive re-publish
|
|
224
|
+
* (`shouldSuppressMaterialize`).
|
|
225
|
+
*/
|
|
226
|
+
export interface ICampaignLastMaterialize {
|
|
227
|
+
status: "running" | "done" | "error";
|
|
228
|
+
/** Required — every one of the four write-sites sets it, and the suppression guard
|
|
229
|
+
* reads it. Firestore returns a Timestamp here: `.toDate()` before comparing. */
|
|
230
|
+
at: Date;
|
|
231
|
+
/** Written EXPLICITLY as null on a run without one — a caveat from the previous run
|
|
232
|
+
* must not survive the next (the stamp is a deep merge). */
|
|
233
|
+
error?: string | null;
|
|
234
|
+
warning?: CampaignMaterializeWarning | null;
|
|
235
|
+
/** Result snapshot (status='done' only) — mirrors counters.unique / counters.eligible. */
|
|
236
|
+
unique?: number;
|
|
237
|
+
eligible?: number;
|
|
238
|
+
}
|
|
194
239
|
export interface ICampaign extends IFireDoc {
|
|
195
240
|
name: string;
|
|
196
241
|
description?: string | null;
|
|
@@ -215,6 +260,9 @@ export interface ICampaign extends IFireDoc {
|
|
|
215
260
|
prevalidation?: ICampaignPrevalidation;
|
|
216
261
|
cost_estimate?: ICampaignCostEstimate;
|
|
217
262
|
counters?: ICampaignCounters;
|
|
263
|
+
/** Cleared (field delete) when the last bucket is removed — an audience of zero
|
|
264
|
+
* must not keep advertising the numbers of the previous run. */
|
|
265
|
+
last_materialize?: ICampaignLastMaterialize;
|
|
218
266
|
created_by?: FirestoreDocumentReference;
|
|
219
267
|
updated_by?: FirestoreDocumentReference;
|
|
220
268
|
scheduled_by?: FirestoreDocumentReference;
|
|
@@ -245,9 +293,13 @@ export interface ICampaignBucketCsvFile {
|
|
|
245
293
|
}>;
|
|
246
294
|
}
|
|
247
295
|
export interface ICampaignBucketCounts {
|
|
296
|
+
/** Rows enumerated by this bucket (CSV: row_count; Algolia: preview/enumeration count). */
|
|
248
297
|
raw?: number;
|
|
298
|
+
/** CSV only, at upload: parseable contacts the bucket contributes. */
|
|
249
299
|
unique_contribution?: number;
|
|
300
|
+
/** Set per bucket by the materializer (dot-path update, never set/merge). */
|
|
250
301
|
eligible?: number;
|
|
302
|
+
/** CSV only, at upload: rows rejected by the parser. */
|
|
251
303
|
invalid?: number;
|
|
252
304
|
opt_out?: number;
|
|
253
305
|
frequency_excluded?: number;
|
|
@@ -263,8 +315,15 @@ export interface ICampaignBucket extends IFireDoc {
|
|
|
263
315
|
}
|
|
264
316
|
/** Flat field driving the batch queue (indexed). */
|
|
265
317
|
export type RecipientDispatchState = "pending" | "dispatched" | "done" | "excluded"
|
|
266
|
-
/**
|
|
267
|
-
*
|
|
318
|
+
/**
|
|
319
|
+
* RESERVED — no writer today. `cancelCampaign` still parks pending recipients as
|
|
320
|
+
* `excluded` and carries the cancellation in `interaction_status='cancelado'`, so
|
|
321
|
+
* this value never reaches Firestore. Kept because the FSM wants it, but whoever
|
|
322
|
+
* starts writing it must check every `dispatch_state` reader first: the queue query
|
|
323
|
+
* filters on `=='pending'` (unaffected), but the FE parses recipients through
|
|
324
|
+
* `zRecipientDispatchStateSchema` and a value missing from that enum makes it drop
|
|
325
|
+
* the WHOLE document, not just the field.
|
|
326
|
+
*/
|
|
268
327
|
| "canceled";
|
|
269
328
|
/** Per-channel delivery status of a recipient (monotonic). */
|
|
270
329
|
export type RecipientChannelStatus = "pending" | "scheduled" | "sent" | "delivered" | "read" | "failed" | "clicked" | "replied";
|
|
@@ -276,6 +335,13 @@ export interface ICampaignRecipientContact {
|
|
|
276
335
|
email?: string;
|
|
277
336
|
external_id?: string;
|
|
278
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Per-channel delivery state. The six milestones (sent/delivered/read/clicked/
|
|
340
|
+
* replied/failed) are FACTS, each with its own timestamp — and the timestamp, not the
|
|
341
|
+
* label, is the idempotency key of the tracking consumers. `status` is a monotonic
|
|
342
|
+
* LABEL derived from them and is shared mutable state, so it cannot serve as a stamp:
|
|
343
|
+
* a late event that moves the label would silently un-stamp the milestone under it.
|
|
344
|
+
*/
|
|
279
345
|
export interface ICampaignRecipientChannelState {
|
|
280
346
|
status: RecipientChannelStatus;
|
|
281
347
|
task_id?: string;
|
|
@@ -285,15 +351,41 @@ export interface ICampaignRecipientChannelState {
|
|
|
285
351
|
read_at?: Date | null;
|
|
286
352
|
clicked_at?: Date | null;
|
|
287
353
|
replied_at?: Date | null;
|
|
354
|
+
/** Milestone stamp of a provider failure. Completes the six — `failed` was the one
|
|
355
|
+
* milestone without a timestamp, so its consumer had to key idempotency off
|
|
356
|
+
* `status === 'failed'` instead (F3.1). */
|
|
357
|
+
failed_at?: Date | null;
|
|
358
|
+
/** Provider error text that accompanies `failed_at`. */
|
|
288
359
|
error?: string | null;
|
|
289
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Why a recipient was kept out. Closed union: every value here HAS a writer in
|
|
363
|
+
* `packages/model/src/evo-campaigns/audience/materializer.ts`, and the FE carries a
|
|
364
|
+
* label for each. Adding a value means adding its label in the same slice —
|
|
365
|
+
* `reasonLabel()` falls back to printing the raw code, which is how
|
|
366
|
+
* `frequency_check_unavailable` would have reached the operator as gibberish.
|
|
367
|
+
*/
|
|
368
|
+
export type CampaignExclusionReason =
|
|
369
|
+
/** Contact opted out of the marketing level (feat-067). */
|
|
370
|
+
"optout"
|
|
371
|
+
/** Touched by another marketing campaign inside `frequency.min_days`. */
|
|
372
|
+
| "frequency"
|
|
373
|
+
/** The fatigue check could not read the lead history and failed CLOSED — the
|
|
374
|
+
* contact was excluded as a precaution, NOT for being contacted recently. */
|
|
375
|
+
| "frequency_check_unavailable"
|
|
376
|
+
/** No identifier for this channel (no mobile / no e-mail). */
|
|
377
|
+
| "no_channel"
|
|
378
|
+
/** Removed by hand from the audience — always the value under the `manual` key. */
|
|
379
|
+
| "manual_removed";
|
|
290
380
|
export interface ICampaignRecipientEligibility {
|
|
291
381
|
per_channel: {
|
|
292
382
|
whatsapp?: "eligible" | "excluded";
|
|
293
383
|
email?: "eligible" | "excluded";
|
|
294
384
|
};
|
|
295
|
-
/** channel
|
|
296
|
-
|
|
385
|
+
/** Keys: `whatsapp` / `email` (per-channel) and `manual` (always `manual_removed`).
|
|
386
|
+
* A recipient can carry several at once — `manual` stacks on top of a real
|
|
387
|
+
* per-channel reason, which is what makes a restore keep it ineligible. */
|
|
388
|
+
exclusion_reasons?: Record<string, CampaignExclusionReason>;
|
|
297
389
|
}
|
|
298
390
|
export interface ICampaignRecipient extends IFireDoc {
|
|
299
391
|
lead_id?: string;
|
|
@@ -305,6 +397,13 @@ export interface ICampaignRecipient extends IFireDoc {
|
|
|
305
397
|
csv_variables?: Record<string, string>;
|
|
306
398
|
/** FLAT field for the batch queue (indexed). */
|
|
307
399
|
dispatch_state: RecipientDispatchState;
|
|
400
|
+
/**
|
|
401
|
+
* Removed by hand from the audience (F2.6/F2.7). This is the DETERMINISTIC marker:
|
|
402
|
+
* `eligibility.per_channel` is deliberately kept at its real value so a restore is
|
|
403
|
+
* symmetric, so per_channel alone cannot tell a manual removal from an organic
|
|
404
|
+
* exclusion. Survives re-materialization; deleted (not set false) on restore.
|
|
405
|
+
*/
|
|
406
|
+
manual_excluded?: boolean;
|
|
308
407
|
eligibility: ICampaignRecipientEligibility;
|
|
309
408
|
channels: {
|
|
310
409
|
whatsapp?: ICampaignRecipientChannelState;
|
|
@@ -255,10 +255,27 @@ export interface ICampaignCounters {
|
|
|
255
255
|
unique?: number;
|
|
256
256
|
eligible?: number;
|
|
257
257
|
excluded_optout?: number;
|
|
258
|
+
/** Excluded by the 15-day fatigue rule. INCLUDES `excluded_frequency_unavailable`
|
|
259
|
+
* (the fail-closed subset) — never sum the two, the second is a breakdown of
|
|
260
|
+
* the first. */
|
|
258
261
|
excluded_frequency?: number;
|
|
262
|
+
/**
|
|
263
|
+
* Subset of `excluded_frequency` excluded because the lead's marketing history
|
|
264
|
+
* could NOT be read, so the check failed CLOSED — not because the contact was
|
|
265
|
+
* touched recently. Reported apart because the two lead to opposite actions:
|
|
266
|
+
* "everybody was contacted in the last 15 days" (wait) vs "the history query is
|
|
267
|
+
* broken" (fix the index and re-materialize). Rolling them together made the
|
|
268
|
+
* screen state something actively false (F3.1 review A1).
|
|
269
|
+
*/
|
|
270
|
+
excluded_frequency_unavailable?: number;
|
|
271
|
+
/** Always 0 today — no code path increments it (see audit note at the bottom). */
|
|
259
272
|
excluded_invalid?: number;
|
|
260
273
|
excluded_no_channel?: number;
|
|
261
274
|
excluded_duplicate?: number;
|
|
275
|
+
/** Recipients removed by hand from the audience (`manual_excluded=true`). Written
|
|
276
|
+
* wholesale by the materializer and incremented/decremented by the exclude/restore
|
|
277
|
+
* endpoints. */
|
|
278
|
+
excluded_manual?: number;
|
|
262
279
|
per_channel?: {
|
|
263
280
|
whatsapp?: ICampaignChannelCounters;
|
|
264
281
|
email?: ICampaignChannelCounters;
|
|
@@ -266,6 +283,38 @@ export interface ICampaignCounters {
|
|
|
266
283
|
converted?: number;
|
|
267
284
|
}
|
|
268
285
|
|
|
286
|
+
// ----- Last materialization stamp -------------------------------------------
|
|
287
|
+
|
|
288
|
+
/** Caveat on a materialization that SUCCEEDED but whose result is not what it looks like. */
|
|
289
|
+
export type CampaignMaterializeWarning =
|
|
290
|
+
/** The campaign has no bucket at all — the audience is empty by construction,
|
|
291
|
+
* not because the filters matched nobody. */
|
|
292
|
+
| "no_buckets"
|
|
293
|
+
/** The 15-day fatigue check could not read the lead history and failed closed:
|
|
294
|
+
* the recipients were excluded as a precaution, not for being contacted recently. */
|
|
295
|
+
| "frequency_check_unavailable";
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Progress/outcome stamp of the last audience materialization, written by the
|
|
299
|
+
* `campaign-materialize` handler (deep merge, so `error`/`warning` are written
|
|
300
|
+
* explicitly as null when absent — a stale caveat must not survive the next run).
|
|
301
|
+
* Drives the FE progress indicator and gates the interactive re-publish
|
|
302
|
+
* (`shouldSuppressMaterialize`).
|
|
303
|
+
*/
|
|
304
|
+
export interface ICampaignLastMaterialize {
|
|
305
|
+
status: "running" | "done" | "error";
|
|
306
|
+
/** Required — every one of the four write-sites sets it, and the suppression guard
|
|
307
|
+
* reads it. Firestore returns a Timestamp here: `.toDate()` before comparing. */
|
|
308
|
+
at: Date;
|
|
309
|
+
/** Written EXPLICITLY as null on a run without one — a caveat from the previous run
|
|
310
|
+
* must not survive the next (the stamp is a deep merge). */
|
|
311
|
+
error?: string | null;
|
|
312
|
+
warning?: CampaignMaterializeWarning | null;
|
|
313
|
+
/** Result snapshot (status='done' only) — mirrors counters.unique / counters.eligible. */
|
|
314
|
+
unique?: number;
|
|
315
|
+
eligible?: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
269
318
|
// ----- Campaign aggregate doc -----------------------------------------------
|
|
270
319
|
|
|
271
320
|
export interface ICampaign extends IFireDoc {
|
|
@@ -297,6 +346,9 @@ export interface ICampaign extends IFireDoc {
|
|
|
297
346
|
prevalidation?: ICampaignPrevalidation;
|
|
298
347
|
cost_estimate?: ICampaignCostEstimate;
|
|
299
348
|
counters?: ICampaignCounters;
|
|
349
|
+
/** Cleared (field delete) when the last bucket is removed — an audience of zero
|
|
350
|
+
* must not keep advertising the numbers of the previous run. */
|
|
351
|
+
last_materialize?: ICampaignLastMaterialize;
|
|
300
352
|
|
|
301
353
|
// Audit (who/when per FSM transition)
|
|
302
354
|
created_by?: FirestoreDocumentReference;
|
|
@@ -334,10 +386,18 @@ export interface ICampaignBucketCsvFile {
|
|
|
334
386
|
}
|
|
335
387
|
|
|
336
388
|
export interface ICampaignBucketCounts {
|
|
389
|
+
/** Rows enumerated by this bucket (CSV: row_count; Algolia: preview/enumeration count). */
|
|
337
390
|
raw?: number;
|
|
391
|
+
/** CSV only, at upload: parseable contacts the bucket contributes. */
|
|
338
392
|
unique_contribution?: number;
|
|
393
|
+
/** Set per bucket by the materializer (dot-path update, never set/merge). */
|
|
339
394
|
eligible?: number;
|
|
395
|
+
/** CSV only, at upload: rows rejected by the parser. */
|
|
340
396
|
invalid?: number;
|
|
397
|
+
// The three below have NO writer anywhere today — a per-bucket exclusion breakdown
|
|
398
|
+
// was never implemented (the breakdown lives on the campaign counters instead).
|
|
399
|
+
// Left declared, optional and empty rather than removed: they are the shape a future
|
|
400
|
+
// per-bucket breakdown should take, and nothing reads them meanwhile.
|
|
341
401
|
opt_out?: number;
|
|
342
402
|
frequency_excluded?: number;
|
|
343
403
|
no_channel?: number;
|
|
@@ -360,8 +420,15 @@ export type RecipientDispatchState =
|
|
|
360
420
|
| "dispatched"
|
|
361
421
|
| "done"
|
|
362
422
|
| "excluded"
|
|
363
|
-
/**
|
|
364
|
-
*
|
|
423
|
+
/**
|
|
424
|
+
* RESERVED — no writer today. `cancelCampaign` still parks pending recipients as
|
|
425
|
+
* `excluded` and carries the cancellation in `interaction_status='cancelado'`, so
|
|
426
|
+
* this value never reaches Firestore. Kept because the FSM wants it, but whoever
|
|
427
|
+
* starts writing it must check every `dispatch_state` reader first: the queue query
|
|
428
|
+
* filters on `=='pending'` (unaffected), but the FE parses recipients through
|
|
429
|
+
* `zRecipientDispatchStateSchema` and a value missing from that enum makes it drop
|
|
430
|
+
* the WHOLE document, not just the field.
|
|
431
|
+
*/
|
|
365
432
|
| "canceled";
|
|
366
433
|
|
|
367
434
|
/** Per-channel delivery status of a recipient (monotonic). */
|
|
@@ -397,6 +464,13 @@ export interface ICampaignRecipientContact {
|
|
|
397
464
|
external_id?: string;
|
|
398
465
|
}
|
|
399
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Per-channel delivery state. The six milestones (sent/delivered/read/clicked/
|
|
469
|
+
* replied/failed) are FACTS, each with its own timestamp — and the timestamp, not the
|
|
470
|
+
* label, is the idempotency key of the tracking consumers. `status` is a monotonic
|
|
471
|
+
* LABEL derived from them and is shared mutable state, so it cannot serve as a stamp:
|
|
472
|
+
* a late event that moves the label would silently un-stamp the milestone under it.
|
|
473
|
+
*/
|
|
400
474
|
export interface ICampaignRecipientChannelState {
|
|
401
475
|
status: RecipientChannelStatus;
|
|
402
476
|
task_id?: string;
|
|
@@ -406,16 +480,43 @@ export interface ICampaignRecipientChannelState {
|
|
|
406
480
|
read_at?: Date | null;
|
|
407
481
|
clicked_at?: Date | null;
|
|
408
482
|
replied_at?: Date | null;
|
|
483
|
+
/** Milestone stamp of a provider failure. Completes the six — `failed` was the one
|
|
484
|
+
* milestone without a timestamp, so its consumer had to key idempotency off
|
|
485
|
+
* `status === 'failed'` instead (F3.1). */
|
|
486
|
+
failed_at?: Date | null;
|
|
487
|
+
/** Provider error text that accompanies `failed_at`. */
|
|
409
488
|
error?: string | null;
|
|
410
489
|
}
|
|
411
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Why a recipient was kept out. Closed union: every value here HAS a writer in
|
|
493
|
+
* `packages/model/src/evo-campaigns/audience/materializer.ts`, and the FE carries a
|
|
494
|
+
* label for each. Adding a value means adding its label in the same slice —
|
|
495
|
+
* `reasonLabel()` falls back to printing the raw code, which is how
|
|
496
|
+
* `frequency_check_unavailable` would have reached the operator as gibberish.
|
|
497
|
+
*/
|
|
498
|
+
export type CampaignExclusionReason =
|
|
499
|
+
/** Contact opted out of the marketing level (feat-067). */
|
|
500
|
+
| "optout"
|
|
501
|
+
/** Touched by another marketing campaign inside `frequency.min_days`. */
|
|
502
|
+
| "frequency"
|
|
503
|
+
/** The fatigue check could not read the lead history and failed CLOSED — the
|
|
504
|
+
* contact was excluded as a precaution, NOT for being contacted recently. */
|
|
505
|
+
| "frequency_check_unavailable"
|
|
506
|
+
/** No identifier for this channel (no mobile / no e-mail). */
|
|
507
|
+
| "no_channel"
|
|
508
|
+
/** Removed by hand from the audience — always the value under the `manual` key. */
|
|
509
|
+
| "manual_removed";
|
|
510
|
+
|
|
412
511
|
export interface ICampaignRecipientEligibility {
|
|
413
512
|
per_channel: {
|
|
414
513
|
whatsapp?: "eligible" | "excluded";
|
|
415
514
|
email?: "eligible" | "excluded";
|
|
416
515
|
};
|
|
417
|
-
/** channel
|
|
418
|
-
|
|
516
|
+
/** Keys: `whatsapp` / `email` (per-channel) and `manual` (always `manual_removed`).
|
|
517
|
+
* A recipient can carry several at once — `manual` stacks on top of a real
|
|
518
|
+
* per-channel reason, which is what makes a restore keep it ineligible. */
|
|
519
|
+
exclusion_reasons?: Record<string, CampaignExclusionReason>;
|
|
419
520
|
}
|
|
420
521
|
|
|
421
522
|
export interface ICampaignRecipient extends IFireDoc {
|
|
@@ -428,6 +529,13 @@ export interface ICampaignRecipient extends IFireDoc {
|
|
|
428
529
|
csv_variables?: Record<string, string>;
|
|
429
530
|
/** FLAT field for the batch queue (indexed). */
|
|
430
531
|
dispatch_state: RecipientDispatchState;
|
|
532
|
+
/**
|
|
533
|
+
* Removed by hand from the audience (F2.6/F2.7). This is the DETERMINISTIC marker:
|
|
534
|
+
* `eligibility.per_channel` is deliberately kept at its real value so a restore is
|
|
535
|
+
* symmetric, so per_channel alone cannot tell a manual removal from an organic
|
|
536
|
+
* exclusion. Survives re-materialization; deleted (not set false) on restore.
|
|
537
|
+
*/
|
|
538
|
+
manual_excluded?: boolean;
|
|
431
539
|
eligibility: ICampaignRecipientEligibility;
|
|
432
540
|
channels: {
|
|
433
541
|
whatsapp?: ICampaignRecipientChannelState;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { IFireGlobalDoc } from '../../shared';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `cancellation_requested`: o cliente pediu o cancelamento e o contrato está em
|
|
4
|
+
* processo de encerramento — mas **continua em vigência** e continua faturando
|
|
5
|
+
* (ver `NEX_FINOPS_BILLABLE_CONTRACT_STATUSES`). Só sai desse estado ao encerrar
|
|
6
|
+
* (`ended`), ao ser anulado (`canceled`) ou se o cliente voltar atrás (`active`).
|
|
7
|
+
*/
|
|
8
|
+
export type NexFinopsContractStatus = 'draft' | 'active' | 'cancellation_requested' | 'inactive' | 'ended' | 'canceled';
|
|
3
9
|
export type NexFinopsContractRecurrence = 'one_time' | 'monthly';
|
|
4
10
|
export type NexFinopsPaymentMethod = 'pix' | 'boleto' | 'payment_link';
|
|
5
11
|
export interface INexFinopsContractServiceItem {
|
|
@@ -25,3 +31,13 @@ export interface INexFinopsContract extends IFireGlobalDoc {
|
|
|
25
31
|
notes?: string | null;
|
|
26
32
|
}
|
|
27
33
|
export declare const NEX_FINOPS_CONTRACT_STATUS_TRANSITIONS: Record<NexFinopsContractStatus, NexFinopsContractStatus[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Status em que um contrato ainda gera cobrança.
|
|
36
|
+
*
|
|
37
|
+
* `cancellation_requested` entra aqui de propósito: o contrato pediu cancelamento
|
|
38
|
+
* mas segue em vigência até encerrar, e deve continuar sendo cobrado no período.
|
|
39
|
+
*
|
|
40
|
+
* Tipado como `readonly string[]` (e não `readonly NexFinopsContractStatus[]`) para
|
|
41
|
+
* que `.includes(x)` aceite qualquer string sem cast no ponto de uso.
|
|
42
|
+
*/
|
|
43
|
+
export declare const NEX_FINOPS_BILLABLE_CONTRACT_STATUSES: readonly string[];
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NEX_FINOPS_CONTRACT_STATUS_TRANSITIONS = void 0;
|
|
3
|
+
exports.NEX_FINOPS_BILLABLE_CONTRACT_STATUSES = exports.NEX_FINOPS_CONTRACT_STATUS_TRANSITIONS = void 0;
|
|
4
4
|
// ── Status transitions ──
|
|
5
5
|
exports.NEX_FINOPS_CONTRACT_STATUS_TRANSITIONS = {
|
|
6
6
|
draft: ['active', 'canceled'],
|
|
7
|
-
active: ['inactive', 'ended', 'canceled'],
|
|
7
|
+
active: ['cancellation_requested', 'inactive', 'ended', 'canceled'],
|
|
8
|
+
// Volta para 'active' cobre o cliente que desiste do cancelamento.
|
|
9
|
+
cancellation_requested: ['ended', 'canceled', 'active'],
|
|
8
10
|
inactive: ['active'],
|
|
9
11
|
ended: [],
|
|
10
12
|
canceled: [],
|
|
11
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Status em que um contrato ainda gera cobrança.
|
|
16
|
+
*
|
|
17
|
+
* `cancellation_requested` entra aqui de propósito: o contrato pediu cancelamento
|
|
18
|
+
* mas segue em vigência até encerrar, e deve continuar sendo cobrado no período.
|
|
19
|
+
*
|
|
20
|
+
* Tipado como `readonly string[]` (e não `readonly NexFinopsContractStatus[]`) para
|
|
21
|
+
* que `.includes(x)` aceite qualquer string sem cast no ponto de uso.
|
|
22
|
+
*/
|
|
23
|
+
exports.NEX_FINOPS_BILLABLE_CONTRACT_STATUSES = ['active', 'cancellation_requested'];
|
|
@@ -2,7 +2,13 @@ import type { IFireGlobalDoc } from '../../shared';
|
|
|
2
2
|
|
|
3
3
|
// ── Status & Recurrence ──
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `cancellation_requested`: o cliente pediu o cancelamento e o contrato está em
|
|
7
|
+
* processo de encerramento — mas **continua em vigência** e continua faturando
|
|
8
|
+
* (ver `NEX_FINOPS_BILLABLE_CONTRACT_STATUSES`). Só sai desse estado ao encerrar
|
|
9
|
+
* (`ended`), ao ser anulado (`canceled`) ou se o cliente voltar atrás (`active`).
|
|
10
|
+
*/
|
|
11
|
+
export type NexFinopsContractStatus = 'draft' | 'active' | 'cancellation_requested' | 'inactive' | 'ended' | 'canceled';
|
|
6
12
|
export type NexFinopsContractRecurrence = 'one_time' | 'monthly';
|
|
7
13
|
export type NexFinopsPaymentMethod = 'pix' | 'boleto' | 'payment_link';
|
|
8
14
|
|
|
@@ -55,8 +61,21 @@ export interface INexFinopsContract extends IFireGlobalDoc {
|
|
|
55
61
|
|
|
56
62
|
export const NEX_FINOPS_CONTRACT_STATUS_TRANSITIONS: Record<NexFinopsContractStatus, NexFinopsContractStatus[]> = {
|
|
57
63
|
draft: ['active', 'canceled'],
|
|
58
|
-
active: ['inactive', 'ended', 'canceled'],
|
|
64
|
+
active: ['cancellation_requested', 'inactive', 'ended', 'canceled'],
|
|
65
|
+
// Volta para 'active' cobre o cliente que desiste do cancelamento.
|
|
66
|
+
cancellation_requested: ['ended', 'canceled', 'active'],
|
|
59
67
|
inactive: ['active'],
|
|
60
68
|
ended: [],
|
|
61
69
|
canceled: [],
|
|
62
70
|
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Status em que um contrato ainda gera cobrança.
|
|
74
|
+
*
|
|
75
|
+
* `cancellation_requested` entra aqui de propósito: o contrato pediu cancelamento
|
|
76
|
+
* mas segue em vigência até encerrar, e deve continuar sendo cobrado no período.
|
|
77
|
+
*
|
|
78
|
+
* Tipado como `readonly string[]` (e não `readonly NexFinopsContractStatus[]`) para
|
|
79
|
+
* que `.includes(x)` aceite qualquer string sem cast no ponto de uso.
|
|
80
|
+
*/
|
|
81
|
+
export const NEX_FINOPS_BILLABLE_CONTRACT_STATUSES: readonly string[] = ['active', 'cancellation_requested'];
|