Campaigns
Campaign zapojí (segment + template + provider) do sendable artefaktu. Tři typy: broadcast (one-off send), triggered (re-fired by event), journey (multi-step — viz Journeys).
Lifecycle
draft → scheduled → sending → sent
draft → cancelled
sending → cancelled (drains in-flight; žádný nový fan-out)
Send fan-out
POST /v1/campaigns/:id/send projde segment v batchích po 1000 a:
- Filtruje — vyloučí self-excluded hráče (capability_mock), hráče přesahující frequency caps, hráče v quiet hours.
- Variant assigns — deterministický FNV-1a hash z
(campaign_id, player_id)→ CDF bucket lookup proticampaign_variants. - Bulk inserts
message_sendsřádků + zařadí BullMQ joby.
Skipnuté řádky jsou s status=failed + error_code=SKIPPED_*. Campaign stats panel je zobrazí jako "Suppressed" místo provider failures.
A/B variants (Sprint 17)
Každá campaign má N variant (tabulka 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 }
]
Migrace backfillne default variantu pro každou existující campaign (referencující template_id té campaign), takže code path je uniform.
Operátoři mohou shiftnout traffic mid-campaign editováním weights — jen nové sends respektují nový split; už-zařazené message_sends drží svůj 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 joinuje message_sends (PG) s raw_events (CH) — pro každého hráče, který dostal zprávu, nejranější goal_event v [sent_at, sent_at + window) se počítá jako konverze.
Stats zahrnují per-variant recipients, conversions, conversion_rate, volitelný 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 joinuje message_sends count per channel ve zvoleném rolling window. Suppressed řádky (SKIPPED_*) se nepočítají do cap.
Quiet hours
tenant.config.messaging.quiet_hours + per-player players.timezone (IANA name). Quiet-window check používá Intl.DateTimeFormat pro DST-aware local-clock konverzi. Hráči ve windowu dostanou error_code=SKIPPED_QUIET_HOURS.