Přeskočit na hlavní obsah

Frequency capping + quiet hours

Two policy gates every send (campaign / journey / manual) goes through before hitting a provider.

Frequency capping

Per-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 counts existing message_sends rows per (player, channel, rolling_window) — only counts rows in (queued, sending, sent, delivered, opened, clicked, bounced) status. Suppressed rows (SKIPPED_*) don't count toward the cap (otherwise cumulative misses would lock you out).

When a campaign batch fan-outs, the cap check runs as a SQL filter directly in the segment query — capped players never enter the batch + don't get a message_sends row at all.

For journey send_message + manual sends (single-player), checkMessagingPolicies() returns the first breached cap.

Quiet hours

Per-tenant config + per-player players.timezone:

{
"messaging": {
"quiet_hours": {
"start": "22:00",
"end": "09:00",
"default_timezone": "Europe/Prague"
}
}
}

Effective timezone resolution: players.timezone (if set) → quiet_hours.default_timezoneUTC. Wrap-around supported (start=22:00 end=09:00 = quiet from 22:00 to 09:00 next day).

Implementation uses Intl.DateTimeFormat with timeZone option for DST-aware local-clock conversion.

When a campaign sends to a player in their quiet window, UCRM doesn't drop the message — it inserts the row with status=failed + error_code=SKIPPED_QUIET_HOURS so the campaign stats panel shows the suppression. Operator can re-target on next campaign run.

Test sends bypass

checkMessagingPolicies({ bypass: true }) skips both checks. The admin "Send test" button uses bypass so operators can test their template without fighting their own policies.

Audit trail

Every suppression writes the reason in message_sends.error_message:

"player in quiet-hours window (Europe/Prague)"
"player has no email for channel email"
"frequency cap email per_day=1 reached"

The recipients list (GET /v1/campaigns/:id/recipients?status=failed) shows them so operators can debug.