Campaigns
Translator note: machine-translated baseline. Needs human review by a native UK speaker. EN source is canonical.
Campaign wires (segment + template + provider) у sendable артефакт. Три типи: broadcast (one-off send), triggered (re-fired by event), journey (multi-step — див. Journeys).
Lifecycle
draft → scheduled → sending → sent
draft → cancelled
sending → cancelled (drains in-flight; немає нового fan-out)
Send fan-out
POST /v1/campaigns/:id/send проходить segment у batches по 1000 і:
- Filters — виключає self-excluded гравців (capability_mock), гравців, що перевищують frequency caps, гравців у quiet hours.
- Variant assigns — детермінований FNV-1a hash з
(campaign_id, player_id)→ CDF bucket lookup протиcampaign_variants. - Bulk inserts
message_sendsrows + enqueue BullMQ jobs.
Skipped rows опиняються з status=failed + error_code=SKIPPED_*. Campaign stats panel показує їх як "Suppressed" замість provider failures.
A/B variants (Sprint 17)
Кожна campaign має N variants (table campaign_variants):
[
{ "name": "Variant A", "label": "A", "template_id": "...", "weight": 50, "sort_order": 0 },
{ "name": "Variant B", "label": "B", "template_id": "...", "weight": 50, "sort_order": 1 }
]
Migration backfill-ить default variant для кожної existing campaign (referencing campaign's own template_id), тож code path uniform.
Оператори можуть shiftити traffic mid-campaign editing weights — тільки нові sends respect новий split; вже-enqueued message_sends тримають assigned variant_id.
Conversion goals (Sprint 17)
{
"goal_event": "deposit_confirmed",
"goal_attribution_days": 7,
"goal_value_property": "amount_base"
}
GET /v1/campaigns/:id/conversion-stats joinить message_sends (PG) з raw_events (CH) — для кожного гравця, що отримав message, найраніший goal_event у [sent_at, sent_at + window) рахується як conversion.
Stats include per-variant recipients, conversions, conversion_rate, опційний conversion_value sum + winner pick (highest rate, min sample 30).
Frequency capping
tenant.config.messaging.frequency_caps:
{
"email": { "per_day": 1, "per_week": 5 },
"sms": { "per_day": 1, "per_week": 3 },
"push": { "per_day": 3, "per_week": 14 }
}
Cap query joinить message_sends count per channel у rolling window. Suppressed rows (SKIPPED_*) не рахуються у cap.
Quiet hours
tenant.config.messaging.quiet_hours + per-player players.timezone (IANA name). Quiet-window check використовує Intl.DateTimeFormat для DST-aware local-clock конверсії. Гравці у window отримують error_code=SKIPPED_QUIET_HOURS.