hub v0.79.0 — R-97c: make the operator-only claim true
v0.78.0 asserted in a comment that a type with no customerMessages entry cannot reach a customer. It can: templates.go falls back to the raw message when the entry is missing, and the only customer gate is prefs.EnabledEvents — pure configuration. A customer with whole_guest_backup_failed enabled would have been emailed raw English operator text about a backup they cannot act on. The new test proves it against the v0.78.0 shape. operatorOnlyEvents is now an explicit register checked before prefs, logging a skipped/operator_only row so the skip is visible. NOT implemented as 'missing customerMessages blocks delivery' — several types rely on that fallback on purpose. The handler comment now names the real mechanism.
This commit is contained in:
@@ -285,7 +285,47 @@ func (d *Dispatcher) processOperator(customerID, eventType, severity, message, d
|
||||
d.store.LogNotification(customerID, eventType, severity, message, "sent", "", "operator")
|
||||
}
|
||||
|
||||
// operatorOnlyEvents are event types that must NEVER reach a customer, whatever their preferences say.
|
||||
//
|
||||
// R-97c. This register exists because the guarantee it provides was previously ASSERTED IN A COMMENT
|
||||
// and not implemented. The claim was that a type with no `customerMessages` entry "structurally
|
||||
// cannot" be routed to a customer. It cannot: `FormatCustomerEmail` (templates.go) treats a missing
|
||||
// entry as a **fallback to the raw message**, not a block —
|
||||
//
|
||||
// hunMessage := customerMessages[eventType]
|
||||
// if hunMessage == "" { hunMessage = message }
|
||||
//
|
||||
// — and the only customer gate is `isEventEnabled(prefs.EnabledEvents, ...)`, i.e. CONFIGURATION.
|
||||
// So a customer with `whole_guest_backup_failed` in their enabled list and an email set would have
|
||||
// received the raw English operator text about a backup they can take no action on.
|
||||
//
|
||||
// That is the `EffectiveProtected` shape: a doc comment claiming a property the code stopped
|
||||
// providing, which is how the samba false alarm survived. The register makes the claim true.
|
||||
//
|
||||
// NOT implemented as "a missing customerMessages entry blocks delivery" — several existing types rely
|
||||
// on the raw-message fallback deliberately (e.g. offbox_enlarge_blocked, whose dynamic Hungarian text
|
||||
// is customer-grade and would be DISCARDED by a template). Turning the fallback into a gate would
|
||||
// change behaviour well outside this concern.
|
||||
var operatorOnlyEvents = map[string]bool{
|
||||
// R-97a. A customer can take no action on a failed whole-guest backup, and being told it failed
|
||||
// while it is still retrying behind the R-88 breaker is alarming without being actionable.
|
||||
"whole_guest_backup_failed": true,
|
||||
// The recovery is ALSO listed, even though its customer leg is pairing-gated on a customer-channel
|
||||
// "sent" row that can never exist for the line above. Relying on that would make this type's safety
|
||||
// a consequence of another type's routing — true today, and silently untrue the moment the failed
|
||||
// event becomes customer-visible. Belt, not inference.
|
||||
"whole_guest_backup_recovered": true,
|
||||
}
|
||||
|
||||
func (d *Dispatcher) processCustomer(customerID, eventType, severity, message, detailsJSON, source string) {
|
||||
// R-97c: operator-tier events stop here, BEFORE prefs are consulted — the point is that no
|
||||
// customer configuration can opt in. Logged rather than dropped, so the skip is visible in
|
||||
// notification_log instead of looking like a delivery that never happened.
|
||||
if operatorOnlyEvents[eventType] {
|
||||
d.store.LogNotification(customerID, eventType, severity, message, "skipped", "operator_only", "customer")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if customer is blocked
|
||||
if d.store.IsCustomerBlocked(customerID) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user